home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / feedparser.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  111.9 KB  |  2,680 lines

  1. #!/usr/bin/env python
  2. """Universal feed parser
  3.  
  4. Handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom feeds
  5.  
  6. Visit http://feedparser.org/ for the latest version
  7. Visit http://feedparser.org/docs/ for the latest documentation
  8.  
  9. Required: Python 2.1 or later
  10. Recommended: Python 2.3 or later
  11. Recommended: CJKCodecs and iconv_codec <http://cjkpython.i18n.org/>
  12. """
  13.  
  14. #__version__ = "pre-3.3-" + "$Revision: 1100 $"[11:15] + "-cvs"
  15. __version__ = "3.3"
  16. __license__ = "Python"
  17. __copyright__ = "Copyright 2002-4, Mark Pilgrim"
  18. __author__ = "Mark Pilgrim <http://diveintomark.org/>"
  19. __contributors__ = ["Jason Diamond <http://injektilo.org/>",
  20.                     "John Beimler <http://john.beimler.org/>",
  21.                     "Fazal Majid <http://www.majid.info/mylos/weblog/>",
  22.                     "Aaron Swartz <http://aaronsw.com>"]
  23. _debug = 0
  24.  
  25. # HTTP "User-Agent" header to send to servers when downloading feeds.
  26. # If you are embedding feedparser in a larger application, you should
  27. # change this to your application name and URL.
  28. USER_AGENT = "UniversalFeedParser/%s +http://feedparser.org/" % __version__
  29. # GRS: And lo, I have done so.
  30. import config
  31. USER_AGENT += " %s/%s (%s)" % \
  32.     (config.get(config.SHORT_APP_NAME),
  33.      config.get(config.APP_VERSION),
  34.      config.get(config.PROJECT_URL))
  35.  
  36. # HTTP "Accept" header to send to servers when downloading feeds.  If you don't
  37. # want to send an Accept header, set this to None.
  38. ACCEPT_HEADER = "application/atom+xml,application/rdf+xml,application/rss+xml,application/x-netcdf,application/xml;q=0.9,text/xml;q=0.2,*/*;q=0.1"
  39.  
  40. # List of preferred XML parsers, by SAX driver name.  These will be tried first,
  41. # but if they're not installed, Python will keep searching through its own list
  42. # of pre-installed parsers until it finds one that supports everything we need.
  43. PREFERRED_XML_PARSERS = ["drv_libxml2"]
  44.  
  45. # If you want feedparser to automatically run HTML markup through HTML Tidy, set
  46. # this to 1.  This is off by default because of reports of crashing on some
  47. # platforms.  If it crashes for you, please submit a bug report with your OS
  48. # platform, Python version, and the URL of the feed you were attempting to parse.
  49. # Requires mxTidy <http://www.egenix.com/files/python/mxTidy.html>
  50. TIDY_MARKUP = 0
  51.  
  52. # ---------- required modules (should come with any Python distribution) ----------
  53. import sgmllib, re, sys, copy, urlparse, time, rfc822, types, cgi
  54. try:
  55.     from cStringIO import StringIO as _StringIO
  56. except:
  57.     from StringIO import StringIO as _StringIO
  58.  
  59. # ---------- optional modules (feedparser will work without these, but with reduced functionality) ----------
  60.  
  61. # gzip is included with most Python distributions, but may not be available if you compiled your own
  62. try:
  63.     import gzip
  64. except:
  65.     gzip = None
  66. try:
  67.     import zlib
  68. except:
  69.     zlib = None
  70.     
  71. # timeoutsocket allows feedparser to time out rather than hang forever on ultra-slow servers.
  72. # Python 2.3 now has this functionality available in the standard socket library, so under
  73. # 2.3 you don't need to install anything.  But you probably should anyway, because the socket
  74. # module is buggy and timeoutsocket is better.
  75. try:
  76.     import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
  77.     timeoutsocket.setDefaultSocketTimeout(20)
  78. except ImportError:
  79.     import socket
  80.     if hasattr(socket, 'setdefaulttimeout'):
  81.         socket.setdefaulttimeout(20)
  82. import urllib, urllib2
  83.  
  84. _mxtidy = None
  85. if TIDY_MARKUP:
  86.     try:
  87.         from mx.Tidy import Tidy as _mxtidy
  88.     except:
  89.         pass
  90.  
  91. # If a real XML parser is available, feedparser will attempt to use it.  feedparser has
  92. # been tested with the built-in SAX parser, PyXML, and libxml2.  On platforms where the
  93. # Python distribution does not come with an XML parser (such as Mac OS X 10.2 and some
  94. # versions of FreeBSD), feedparser will quietly fall back on regex-based parsing.
  95. try:
  96.     import xml.sax
  97.     xml.sax.make_parser(PREFERRED_XML_PARSERS) # test for valid parsers
  98.     from xml.sax.saxutils import escape as _xmlescape
  99.     _XML_AVAILABLE = 1
  100. except:
  101.     _XML_AVAILABLE = 0
  102.     def _xmlescape(data):
  103.         data = data.replace("&", "&")
  104.         data = data.replace(">", ">")
  105.         data = data.replace("<", "<")
  106.         return data
  107.  
  108. # base64 support for Atom feeds that contain embedded binary data
  109. try:
  110.     import base64, binascii
  111. except:
  112.     base64 = binascii = None
  113.  
  114. # cjkcodecs and iconv_codec provide support for more character encodings.
  115. # Both are available from http://cjkpython.i18n.org/
  116. try:
  117.     import cjkcodecs.aliases
  118. except:
  119.     pass
  120. try:
  121.     import iconv_codec
  122. except:
  123.     pass
  124.  
  125. # ---------- don't touch these ----------
  126. class CharacterEncodingOverride(Exception): pass
  127. class CharacterEncodingUnknown(Exception): pass
  128. class NonXMLContentType(Exception): pass
  129.  
  130. sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*')
  131. sgmllib.special = re.compile('<!')
  132. sgmllib.charref = re.compile('&#(x?[0-9A-Fa-f]+)[^0-9A-Fa-f]')
  133.  
  134. SUPPORTED_VERSIONS = {'': 'unknown',
  135.                       'rss090': 'RSS 0.90',
  136.                       'rss091n': 'RSS 0.91 (Netscape)',
  137.                       'rss091u': 'RSS 0.91 (Userland)',
  138.                       'rss092': 'RSS 0.92',
  139.                       'rss093': 'RSS 0.93',
  140.                       'rss094': 'RSS 0.94',
  141.                       'rss20': 'RSS 2.0',
  142.                       'rss10': 'RSS 1.0',
  143.                       'rss': 'RSS (unknown version)',
  144.                       'atom01': 'Atom 0.1',
  145.                       'atom02': 'Atom 0.2',
  146.                       'atom03': 'Atom 0.3',
  147.                       'atom': 'Atom (unknown version)',
  148.                       'cdf': 'CDF',
  149.                       'hotrss': 'Hot RSS'
  150.                       }
  151.  
  152. try:
  153.     UserDict = dict
  154. except NameError:
  155.     # Python 2.1 does not have dict
  156.     from UserDict import UserDict
  157.     def dict(aList):
  158.         rc = {}
  159.         for k, v in aList:
  160.             rc[k] = v
  161.         return rc
  162.  
  163. class FeedParserDict(UserDict):
  164.     def __getitem__(self, key):
  165.         keymap = {'channel': 'feed',
  166.                   'items': 'entries',
  167.                   'guid': 'id',
  168.                   'length': ['filesize','length'],
  169.                   'date': 'modified',
  170.                   'date_parsed': 'modified_parsed',
  171.                   'description': ['tagline', 'summary']}
  172.         realkey = keymap.get(key, key)
  173.         if type(realkey) == types.ListType:
  174.             for k in realkey:
  175.                 if UserDict.has_key(self, k):
  176.                     return UserDict.__getitem__(self, k)
  177.             return UserDict.__getitem__(self, key)
  178.         return UserDict.__getitem__(self, realkey)
  179.  
  180.     def has_key(self, key):
  181.         return hasattr(self, key) or UserDict.has_key(self, key)
  182.         
  183.     def __getattr__(self, key):
  184.         try:
  185.             return self.__dict__[key]
  186.         except KeyError:
  187.             pass
  188.         try:
  189.             return self.__getitem__(key)
  190.         except:
  191.             raise AttributeError, "object has no attribute '%s'" % key
  192.  
  193.     def __contains__(self, key):
  194.         return self.has_key(key)
  195.  
  196. def zopeCompatibilityHack():
  197.     global FeedParserDict
  198.     del FeedParserDict
  199.     def FeedParserDict(aDict=None):
  200.         rc = {}
  201.         if aDict:
  202.             rc.update(aDict)
  203.         return rc
  204.  
  205. _ebcdic_to_ascii_map = None
  206. def _ebcdic_to_ascii(s):
  207.     global _ebcdic_to_ascii_map
  208.     if not _ebcdic_to_ascii_map:
  209.         emap = (
  210.             0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15,
  211.             16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31,
  212.             128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7,
  213.             144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26,
  214.             32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33,
  215.             38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94,
  216.             45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63,
  217.             186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34,
  218.             195,97,98,99,100,101,102,103,104,105,196,197,198,199,200,201,
  219.             202,106,107,108,109,110,111,112,113,114,203,204,205,206,207,208,
  220.             209,126,115,116,117,118,119,120,121,122,210,211,212,213,214,215,
  221.             216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,
  222.             123,65,66,67,68,69,70,71,72,73,232,233,234,235,236,237,
  223.             125,74,75,76,77,78,79,80,81,82,238,239,240,241,242,243,
  224.             92,159,83,84,85,86,87,88,89,90,244,245,246,247,248,249,
  225.             48,49,50,51,52,53,54,55,56,57,250,251,252,253,254,255
  226.             )
  227.         import string
  228.         _ebcdic_to_ascii_map = string.maketrans( \
  229.             "".join(map(chr, range(256))), "".join(map(chr, emap)))
  230.     return s.translate(_ebcdic_to_ascii_map)
  231.  
  232. class _FeedParserMixin:
  233.     namespaces = {"": "",
  234.                   "http://backend.userland.com/rss": "",
  235.                   "http://blogs.law.harvard.edu/tech/rss": "",
  236.                   "http://purl.org/rss/1.0/": "",
  237.                   "http://my.netscape.com/rdf/simple/0.9/": "",
  238.                   "http://example.com/newformat#": "",
  239.                   "http://example.com/necho": "",
  240.                   "http://purl.org/echo/": "",
  241.                   "uri/of/echo/namespace#": "",
  242.                   "http://purl.org/pie/": "",
  243.                   "http://purl.org/atom/ns#": "",
  244.                   "http://purl.org/rss/1.0/modules/rss091#": "",
  245.                   
  246.                   "http://webns.net/mvcb/":                               "admin",
  247.                   "http://purl.org/rss/1.0/modules/aggregation/":         "ag",
  248.                   "http://purl.org/rss/1.0/modules/annotate/":            "annotate",
  249.                   "http://media.tangent.org/rss/1.0/":                    "audio",
  250.                   "http://backend.userland.com/blogChannelModule":        "blogChannel",
  251.                   "http://web.resource.org/cc/":                          "cc",
  252.                   "http://backend.userland.com/creativeCommonsRssModule": "creativeCommons",
  253.                   "http://purl.org/rss/1.0/modules/company":              "co",
  254.                   "http://purl.org/rss/1.0/modules/content/":             "content",
  255.                   "http://my.theinfo.org/changed/1.0/rss/":               "cp",
  256.                   "http://purl.org/dc/elements/1.1/":                     "dc",
  257.                   "http://purl.org/dc/terms/":                            "dcterms",
  258.                   "http://purl.org/rss/1.0/modules/email/":               "email",
  259.                   "http://purl.org/rss/1.0/modules/event/":               "ev",
  260.                   "http://postneo.com/icbm/":                             "icbm",
  261.                   "http://purl.org/rss/1.0/modules/image/":               "image",
  262.                   "http://xmlns.com/foaf/0.1/":                           "foaf",
  263.                   "http://freshmeat.net/rss/fm/":                         "fm",
  264.                   "http://purl.org/rss/1.0/modules/link/":                "l",
  265.                   "http://madskills.com/public/xml/rss/module/pingback/": "pingback",
  266.                   "http://prismstandard.org/namespaces/1.2/basic/":       "prism",
  267.                   "http://www.w3.org/1999/02/22-rdf-syntax-ns#":          "rdf",
  268.                   "http://www.w3.org/2000/01/rdf-schema#":                "rdfs",
  269.                   "http://purl.org/rss/1.0/modules/reference/":           "ref",
  270.                   "http://purl.org/rss/1.0/modules/richequiv/":           "reqv",
  271.                   "http://purl.org/rss/1.0/modules/search/":              "search",
  272.                   "http://purl.org/rss/1.0/modules/slash/":               "slash",
  273.                   "http://purl.org/rss/1.0/modules/servicestatus/":       "ss",
  274.                   "http://hacks.benhammersley.com/rss/streaming/":        "str",
  275.                   "http://purl.org/rss/1.0/modules/subscription/":        "sub",
  276.                   "http://purl.org/rss/1.0/modules/syndication/":         "sy",
  277.                   "http://purl.org/rss/1.0/modules/taxonomy/":            "taxo",
  278.                   "http://purl.org/rss/1.0/modules/threading/":           "thr",
  279.                   "http://purl.org/rss/1.0/modules/textinput/":           "ti",
  280.                   "http://madskills.com/public/xml/rss/module/trackback/":"trackback",
  281.                   "http://wellformedweb.org/CommentAPI/":                 "wfw",
  282.                   "http://purl.org/rss/1.0/modules/wiki/":                "wiki",
  283.                   "http://schemas.xmlsoap.org/soap/envelope/":            "soap",
  284.                   "http://www.w3.org/1999/xhtml":                         "xhtml",
  285.                   "http://www.w3.org/XML/1998/namespace":                 "xml",
  286.                   "http://tools.search.yahoo.com/mrss/":                  "media",
  287.                   "http://docs.yahoo.com/mediaModule":                    "media",
  288.                   "http://search.yahoo.com/mrss":                         "media",
  289.                   "http://participatoryculture.org/RSSModules/dtv/1.0":   "dtv"
  290. }
  291.  
  292.     can_be_relative_uri = ['link', 'id', 'wfw_comment', 'wfw_commentrss', 'docs', 'url', 'comments', 'license']
  293.     can_contain_relative_uris = ['content', 'title', 'summary', 'info', 'tagline', 'copyright', 'description']
  294.     can_contain_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'copyright', 'description']
  295.     html_types = ['text/html', 'application/xhtml+xml']
  296.     
  297.     def __init__(self, baseuri=None, baselang=None, encoding='utf-8'):
  298.         if _debug: sys.stderr.write("initializing FeedParser\n")
  299.         self.feeddata = FeedParserDict() # feed-level data
  300.         self.encoding = encoding # character encoding
  301.         self.entries = [] # list of entry-level data
  302.         self.version = '' # feed type/version, see SUPPORTED_VERSIONS
  303.  
  304.         # the following are used internally to track state;
  305.         # some of this is kind of out of control and should
  306.         # probably be refactored into a finite state machine
  307.         self.infeed = 0
  308.         self.inentry = 0
  309.         self.incontent = 0
  310.         self.intextinput = 0
  311.         self.inimage = 0
  312.         self.inauthor = 0
  313.         self.incontributor = 0
  314.         self.inenclosure = 0
  315.         self.contentparams = FeedParserDict()
  316.         self.namespacemap = {}
  317.         self.elementstack = []
  318.         self.basestack = []
  319.         self.langstack = []
  320.         self.baseuri = baseuri or ''
  321.         self.lang = baselang or None
  322.         if baselang:
  323.             self.feeddata['language'] = baselang
  324.  
  325.     def unknown_starttag(self, tag, attrs):
  326.         if _debug: sys.stderr.write('start %s with %s\n' % (tag, attrs))
  327.         # normalize attrs
  328.         attrs = [(k.lower(), v) for k, v in attrs]
  329.         attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
  330.         
  331.         # track xml:base and xml:lang
  332.         attrsD = FeedParserDict(attrs)
  333.         baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri
  334.         self.baseuri = baseuri
  335.         lang = attrsD.get('xml:lang', attrsD.get('lang'))
  336.         if lang == '':
  337.             # xml:lang could be explicitly set to '', we need to capture that
  338.             lang = None
  339.         elif lang is None:
  340.             # if no xml:lang is specified, use parent lang
  341.             lang = self.lang
  342.         if lang:
  343.             if tag in ('feed', 'rss', 'rdf:RDF'):
  344.                 self.feeddata['language'] = lang
  345.         self.lang = lang
  346.         self.basestack.append(baseuri)
  347.         self.langstack.append(lang)
  348.         
  349.         # track namespaces
  350.         for prefix, uri in attrs:
  351.             if prefix.startswith('xmlns:'):
  352.                 self.trackNamespace(prefix[6:], uri)
  353.             elif prefix == 'xmlns':
  354.                 self.trackNamespace(None, uri)
  355.  
  356.         # track inline content
  357.         if self.incontent and self.contentparams.get('mode') == 'escaped':
  358.             # element declared itself as escaped markup, but it isn't really
  359.             self.contentparams['mode'] = 'xml'
  360.         if self.incontent and self.contentparams.get('mode') == 'xml':
  361.             # Note: probably shouldn't simply recreate localname here, but
  362.             # our namespace handling isn't actually 100% correct in cases where
  363.             # the feed redefines the default namespace (which is actually
  364.             # the usual case for inline content, thanks Sam), so here we
  365.             # cheat and just reconstruct the element based on localname
  366.             # because that compensates for the bugs in our namespace handling.
  367.             # This will horribly munge inline content with non-empty qnames,
  368.             # but nobody actually does that, so I'm not fixing it.
  369.             tag = tag.split(':')[-1]
  370.             return self.handle_data("<%s%s>" % (tag, "".join([' %s="%s"' % t for t in attrs])), escape=0)
  371.  
  372.         # match namespaces
  373.         if tag.find(':') <> -1:
  374.             prefix, suffix = tag.split(':', 1)
  375.         else:
  376.             prefix, suffix = '', tag
  377.         prefix = self.namespacemap.get(prefix, prefix)
  378.         if prefix:
  379.             prefix = prefix + '_'
  380.  
  381.         # special hack for better tracking of empty textinput/image elements in illformed feeds
  382.         if (not prefix) and tag not in ('title', 'link', 'description', 'name'):
  383.             self.intextinput = 0
  384.         if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'width', 'height'):
  385.             self.inimage = 0
  386.         
  387.         # call special handler (if defined) or default handler
  388.         methodname = '_start_' + prefix + suffix
  389.         try:
  390.             method = getattr(self, methodname)
  391.             return method(attrsD)
  392.         except AttributeError:
  393.             return self.push(prefix + suffix, 1)
  394.  
  395.     def unknown_endtag(self, tag):
  396.         if _debug: sys.stderr.write('end %s\n' % tag)
  397.         # match namespaces
  398.         if tag.find(':') <> -1:
  399.             prefix, suffix = tag.split(':', 1)
  400.         else:
  401.             prefix, suffix = '', tag
  402.         prefix = self.namespacemap.get(prefix, prefix)
  403.         if prefix:
  404.             prefix = prefix + '_'
  405.  
  406.         # call special handler (if defined) or default handler
  407.         methodname = '_end_' + prefix + suffix
  408.         try:
  409.             method = getattr(self, methodname)
  410.             method()
  411.         except AttributeError:
  412.             self.pop(prefix + suffix)
  413.  
  414.         # track inline content
  415.         if self.incontent and self.contentparams.get('mode') == 'escaped':
  416.             # element declared itself as escaped markup, but it isn't really
  417.             self.contentparams['mode'] = 'xml'
  418.         if self.incontent and self.contentparams.get('mode') == 'xml':
  419.             tag = tag.split(':')[-1]
  420.             self.handle_data("</%s>" % tag, escape=0)
  421.  
  422.         # track xml:base and xml:lang going out of scope
  423.         if self.basestack:
  424.             self.basestack.pop()
  425.             if self.basestack and self.basestack[-1]:
  426.                 self.baseuri = self.basestack[-1]
  427.         if self.langstack:
  428.             self.langstack.pop()
  429.             if self.langstack: # and (self.langstack[-1] is not None):
  430.                 self.lang = self.langstack[-1]
  431.  
  432.     def handle_charref(self, ref):
  433.         # called for each character reference, e.g. for " ", ref will be "160"
  434.         if not self.elementstack: return
  435.         ref = ref.lower()
  436.         if ref in ('34', '38', '39', '60', '62', 'x22', 'x26', 'x27', 'x3c', 'x3e'):
  437.             text = "&#%s;" % ref
  438.         else:
  439.             if ref[0] == 'x':
  440.                 c = int(ref[1:], 16)
  441.             else:
  442.                 c = int(ref)
  443.             text = unichr(c).encode('utf-8')
  444.         self.elementstack[-1][2].append(text)
  445.  
  446.     def handle_entityref(self, ref):
  447.         # called for each entity reference, e.g. for "©", ref will be "copy"
  448.         if not self.elementstack: return
  449.         if _debug: sys.stderr.write("entering handle_entityref with %s\n" % ref)
  450.         if ref in ('lt', 'gt', 'quot', 'amp', 'apos'):
  451.             text = '&%s;' % ref
  452.         else:
  453.             # entity resolution graciously donated by Aaron Swartz
  454.             def name2cp(k):
  455.                 import htmlentitydefs
  456.                 if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3
  457.                     return htmlentitydefs.name2codepoint[k]
  458.                 k = htmlentitydefs.entitydefs[k]
  459.                 if k.startswith("&#") and k.endswith(";"):
  460.                     return int(k[2:-1]) # not in latin-1
  461.                 return ord(k)
  462.             try: name2cp(ref)
  463.             except KeyError: text = "&%s;" % ref
  464.             else: text = unichr(name2cp(ref)).encode('utf-8')
  465.         self.elementstack[-1][2].append(text)
  466.  
  467.     def handle_data(self, text, escape=1):
  468.         # called for each block of plain text, i.e. outside of any tag and
  469.         # not containing any character or entity references
  470.         if not self.elementstack: return
  471.         if escape and self.contentparams.get('mode') == 'xml':
  472.             text = _xmlescape(text)
  473.         self.elementstack[-1][2].append(text)
  474.  
  475.     def handle_comment(self, text):
  476.         # called for each comment, e.g. <!-- insert message here -->
  477.         pass
  478.  
  479.     def handle_pi(self, text):
  480.         # called for each processing instruction, e.g. <?instruction>
  481.         pass
  482.  
  483.     def handle_decl(self, text):
  484.         pass
  485.  
  486.     def parse_declaration(self, i):
  487.         # override internal declaration handler to handle CDATA blocks
  488.         if _debug: sys.stderr.write("entering parse_declaration\n")
  489.         if self.rawdata[i:i+9] == '<![CDATA[':
  490.             k = self.rawdata.find(']]>', i)
  491.             if k == -1: k = len(self.rawdata)
  492.             self.handle_data(_xmlescape(self.rawdata[i+9:k]), 0)
  493.             return k+3
  494.         else:
  495.             k = self.rawdata.find('>', i)
  496.             return k+1
  497.  
  498.     def trackNamespace(self, prefix, uri):
  499.         if (prefix, uri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version:
  500.             self.version = 'rss090'
  501.         if uri == 'http://purl.org/rss/1.0/' and not self.version:
  502.             self.version = 'rss10'
  503.         if not prefix: return
  504.         if uri.find('backend.userland.com/rss') <> -1:
  505.             # match any backend.userland.com namespace
  506.             uri = 'http://backend.userland.com/rss'
  507.         if self.namespaces.has_key(uri):
  508.             self.namespacemap[prefix] = self.namespaces[uri]
  509.  
  510.     def resolveURI(self, uri):
  511.         return urlparse.urljoin(self.baseuri or '', uri)
  512.     
  513.     def decodeEntities(self, element, data):
  514.         return data
  515.  
  516.     def push(self, element, expectingText):
  517.         self.elementstack.append([element, expectingText, []])
  518.  
  519.     def pop(self, element):
  520.         if not self.elementstack: return
  521.         if self.elementstack[-1][0] != element: return
  522.  
  523.         element, expectingText, pieces = self.elementstack.pop()
  524.         output = "".join(pieces)
  525.         output = output.strip()
  526.         if not expectingText: return output
  527.         
  528.         # decode base64 content
  529.         if self.contentparams.get('mode') == 'base64' and base64:
  530.             try:
  531.                 output = base64.decodestring(output)
  532.             except binascii.Error:
  533.                 pass
  534.             except binascii.Incomplete:
  535.                 pass
  536.                 
  537.         # resolve relative URIs
  538.         if (element in self.can_be_relative_uri) and output:
  539.             output = self.resolveURI(output)
  540.         
  541.         # decode entities within embedded markup
  542.         output = self.decodeEntities(element, output)
  543.  
  544.         # resolve relative URIs within embedded markup
  545.         if self.contentparams.get('type', 'text/html') in self.html_types:
  546.             if element in self.can_contain_relative_uris:
  547.                 output = _resolveRelativeURIs(output, self.baseuri, self.encoding)
  548.         
  549.         # sanitize embedded markup
  550.         if self.contentparams.get('type', 'text/html') in self.html_types:
  551.             if element in self.can_contain_dangerous_markup:
  552.                 output = _sanitizeHTML(output, self.encoding)
  553.  
  554.         if self.encoding and (type(output) == types.StringType):
  555.             try:
  556.                 output = unicode(output, self.encoding)
  557.             except:
  558.                 pass
  559.             
  560.         # store output in appropriate place(s)
  561.         if self.inentry:
  562.             if element == 'content':
  563.                 self.entries[-1].setdefault(element, [])
  564.                 contentparams = copy.deepcopy(self.contentparams)
  565.                 contentparams['value'] = output
  566.                 self.entries[-1][element].append(contentparams)
  567.             elif element == 'category':
  568.                 self.entries[-1][element] = output
  569.                 domain = self.entries[-1]['categories'][-1][0]
  570.                 self.entries[-1]['categories'][-1] = (domain, output)
  571.             elif element == 'source':
  572.                 self.entries[-1]['source']['value'] = output
  573.             elif element == 'link':
  574.                 self.entries[-1][element] = output
  575.                 if output:
  576.                     self.entries[-1]['links'][-1]['href'] = output
  577.             else:
  578.                 if element == 'description':
  579.                     element = 'summary'
  580.                 self.entries[-1][element] = output
  581.                 if self.incontent:
  582.                     contentparams = copy.deepcopy(self.contentparams)
  583.                     contentparams['value'] = output
  584.                     self.entries[-1][element + '_detail'] = contentparams
  585.         elif self.infeed and (not self.intextinput) and (not self.inimage):
  586.             if element == 'description':
  587.                 element = 'tagline'
  588.             self.feeddata[element] = output
  589.             if element == 'category':
  590.                 domain = self.feeddata['categories'][-1][0]
  591.                 self.feeddata['categories'][-1] = (domain, output)
  592.             elif element == 'link':
  593.                 self.feeddata['links'][-1]['href'] = output
  594.             elif self.incontent:
  595.                 contentparams = copy.deepcopy(self.contentparams)
  596.                 contentparams['value'] = output
  597.                 self.feeddata[element + '_detail'] = contentparams
  598.         return output
  599.  
  600.     def _mapToStandardPrefix(self, name):
  601.         colonpos = name.find(':')
  602.         if colonpos <> -1:
  603.             prefix = name[:colonpos]
  604.             suffix = name[colonpos+1:]
  605.             prefix = self.namespacemap.get(prefix, prefix)
  606.             name = prefix + ':' + suffix
  607.         return name
  608.         
  609.     def _getAttribute(self, attrsD, name):
  610.         return attrsD.get(self._mapToStandardPrefix(name))
  611.  
  612.     def _save(self, key, value):
  613.         if self.inentry:
  614.             self.entries[-1].setdefault(key, value)
  615.         elif self.feeddata:
  616.             self.feeddata.setdefault(key, value)
  617.  
  618.     def _start_rss(self, attrsD):
  619.         versionmap = {'0.91': 'rss091u',
  620.                       '0.92': 'rss092',
  621.                       '0.93': 'rss093',
  622.                       '0.94': 'rss094'}
  623.         if not self.version:
  624.             attr_version = attrsD.get('version', '')
  625.             version = versionmap.get(attr_version)
  626.             if version:
  627.                 self.version = version
  628.             elif attr_version.startswith('2.'):
  629.                 self.version = 'rss20'
  630.             else:
  631.                 self.version = 'rss'
  632.     
  633.     def _start_dlhottitles(self, attrsD):
  634.         self.version = 'hotrss'
  635.  
  636.     def _start_channel(self, attrsD):
  637.         self.infeed = 1
  638.         self._cdf_common(attrsD)
  639.     _start_feedinfo = _start_channel
  640.  
  641.     def _cdf_common(self, attrsD):
  642.         if attrsD.has_key('lastmod'):
  643.             self._start_modified({})
  644.             self.elementstack[-1][-1] = attrsD['lastmod']
  645.             self._end_modified()
  646.         if attrsD.has_key('href'):
  647.             self._start_link({})
  648.             self.elementstack[-1][-1] = attrsD['href']
  649.             self._end_link()
  650.     
  651.     def _start_feed(self, attrsD):
  652.         self.infeed = 1
  653.         versionmap = {'0.1': 'atom01',
  654.                       '0.2': 'atom02',
  655.                       '0.3': 'atom03'}
  656.         if not self.version:
  657.             attr_version = attrsD.get('version')
  658.             version = versionmap.get(attr_version)
  659.             if version:
  660.                 self.version = version
  661.             else:
  662.                 self.version = 'atom'
  663.  
  664.     def _end_channel(self):
  665.         self.infeed = 0
  666.     _end_feed = _end_channel
  667.     
  668.     def _start_image(self, attrsD):
  669.         self.inimage = 1
  670.         self.push('image', 0)
  671.         context = self._getContext()
  672.         context.setdefault('image', FeedParserDict())
  673.             
  674.     def _end_image(self):
  675.         self.pop('image')
  676.         self.inimage = 0
  677.                 
  678.     def _start_textinput(self, attrsD):
  679.         self.intextinput = 1
  680.         self.push('textinput', 0)
  681.         context = self._getContext()
  682.         context.setdefault('textinput', FeedParserDict())
  683.     _start_textInput = _start_textinput
  684.     
  685.     def _end_textinput(self):
  686.         self.pop('textinput')
  687.         self.intextinput = 0
  688.     _end_textInput = _end_textinput
  689.  
  690.     def _start_author(self, attrsD):
  691.         self.inauthor = 1
  692.         self.push('author', 1)
  693.     _start_managingeditor = _start_author
  694.     _start_dc_author = _start_author
  695.     _start_dc_creator = _start_author
  696.  
  697.     def _end_author(self):
  698.         self.pop('author')
  699.         self.inauthor = 0
  700.         self._sync_author_detail()
  701.     _end_managingeditor = _end_author
  702.     _end_dc_author = _end_author
  703.     _end_dc_creator = _end_author
  704.  
  705.     def _start_contributor(self, attrsD):
  706.         self.incontributor = 1
  707.         context = self._getContext()
  708.         context.setdefault('contributors', [])
  709.         context['contributors'].append(FeedParserDict())
  710.         self.push('contributor', 0)
  711.  
  712.     def _end_contributor(self):
  713.         self.pop('contributor')
  714.         self.incontributor = 0
  715.         
  716.     def _start_name(self, attrsD):
  717.         self.push('name', 0)
  718.  
  719.     def _end_name(self):
  720.         value = self.pop('name')
  721.         if self.inauthor:
  722.             self._save_author('name', value)
  723.         elif self.incontributor:
  724.             self._save_contributor('name', value)
  725.         elif self.intextinput:
  726.             context = self._getContext()
  727.             context['textinput']['name'] = value
  728.  
  729.     def _start_width(self, attrsD):
  730.         self.push('width', 0)
  731.  
  732.     def _end_width(self):
  733.         value = self.pop('width')
  734.         try:
  735.             value = int(value)
  736.         except:
  737.             value = 0
  738.         if self.inimage:
  739.             context = self._getContext()
  740.             context['image']['width'] = value
  741.  
  742.     def _start_height(self, attrsD):
  743.         self.push('height', 0)
  744.  
  745.     def _end_height(self):
  746.         value = self.pop('height')
  747.         try:
  748.             value = int(value)
  749.         except:
  750.             value = 0
  751.         if self.inimage:
  752.             context = self._getContext()
  753.             context['image']['height'] = value
  754.  
  755.     def _start_url(self, attrsD):
  756.         self.push('url', 1)
  757.     _start_homepage = _start_url
  758.     _start_uri = _start_url
  759.  
  760.     def _end_url(self):
  761.         value = self.pop('url')
  762.         if self.inauthor:
  763.             self._save_author('url', value)
  764.         elif self.incontributor:
  765.             self._save_contributor('url', value)
  766.         elif self.inimage:
  767.             context = self._getContext()
  768.             context['image']['url'] = value
  769.         elif self.intextinput:
  770.             context = self._getContext()
  771.             context['textinput']['link'] = value
  772.     _end_homepage = _end_url
  773.     _end_uri = _end_url
  774.  
  775.     def _start_email(self, attrsD):
  776.         self.push('email', 0)
  777.  
  778.     def _end_email(self):
  779.         value = self.pop('email')
  780.         if self.inauthor:
  781.             self._save_author('email', value)
  782.         elif self.incontributor:
  783.             self._save_contributor('email', value)
  784.             pass
  785.  
  786.     def _getContext(self):
  787.         if self.inentry:
  788.             context = self.entries[-1]
  789.         else:
  790.             context = self.feeddata
  791.         return context
  792.  
  793.     def _save_author(self, key, value):
  794.         context = self._getContext()
  795.         context.setdefault('author_detail', FeedParserDict())
  796.         context['author_detail'][key] = value
  797.         self._sync_author_detail()
  798.  
  799.     def _save_contributor(self, key, value):
  800.         context = self._getContext()
  801.         context.setdefault('contributors', [FeedParserDict()])
  802.         context['contributors'][-1][key] = value
  803.  
  804.     def _sync_author_detail(self, key='author'):
  805.         context = self._getContext()
  806.         detail = context.get('%s_detail' % key)
  807.         if detail:
  808.             name = detail.get('name')
  809.             email = detail.get('email')
  810.             if name and email:
  811.                 context[key] = "%s (%s)" % (name, email)
  812.             elif name:
  813.                 context[key] = name
  814.             elif email:
  815.                 context[key] = email
  816.         else:
  817.             author = context.get(key)
  818.             if not author: return
  819.             emailmatch = re.search(r"""(([a-zA-Z0-9\_\-\.\+]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?))""", author)
  820.             if not emailmatch: return
  821.             email = emailmatch.group(0)
  822.             # probably a better way to do the following, but it passes all the tests
  823.             author = author.replace(email, '')
  824.             author = author.replace('()', '')
  825.             author = author.strip()
  826.             if author and (author[0] == '('):
  827.                 author = author[1:]
  828.             if author and (author[-1] == ')'):
  829.                 author = author[:-1]
  830.             author = author.strip()
  831.             context.setdefault('%s_detail' % key, FeedParserDict())
  832.             context['%s_detail' % key]['name'] = author
  833.             context['%s_detail' % key]['email'] = email
  834.             
  835.     def _start_tagline(self, attrsD):
  836.         self.incontent += 1
  837.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  838.                               'type': attrsD.get('type', 'text/plain'),
  839.                               'language': self.lang,
  840.                               'base': self.baseuri})
  841.         self.push('tagline', 1)
  842.     _start_subtitle = _start_tagline
  843.  
  844.     def _end_tagline(self):
  845.         value = self.pop('tagline')
  846.         self.incontent -= 1
  847.         self.contentparams.clear()
  848.         if self.infeed:
  849.             self.feeddata['description'] = value
  850.     _end_subtitle = _end_tagline
  851.             
  852.     def _start_copyright(self, attrsD):
  853.         self.incontent += 1
  854.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  855.                               'type': attrsD.get('type', 'text/plain'),
  856.                               'language': self.lang,
  857.                               'base': self.baseuri})
  858.         self.push('copyright', 1)
  859.     _start_dc_rights = _start_copyright
  860.  
  861.     def _end_copyright(self):
  862.         self.pop('copyright')
  863.         self.incontent -= 1
  864.         self.contentparams.clear()
  865.     _end_dc_rights = _end_copyright
  866.  
  867.     def _start_item(self, attrsD):
  868.         self.entries.append(FeedParserDict())
  869.         self.push('item', 0)
  870.         self.inentry = 1
  871.         self.guidislink = 0
  872.         id = self._getAttribute(attrsD, 'rdf:about')
  873.         if id:
  874.             context = self._getContext()
  875.             context['id'] = id
  876.         self._cdf_common(attrsD)
  877.     _start_entry = _start_item
  878.     _start_product = _start_item
  879.  
  880.     def _end_item(self):
  881.         self.pop('item')
  882.         self.inentry = 0
  883.     _end_entry = _end_item
  884.  
  885.     def _start_dc_language(self, attrsD):
  886.         self.push('language', 1)
  887.     _start_language = _start_dc_language
  888.  
  889.     def _end_dc_language(self):
  890.         self.lang = self.pop('language')
  891.     _end_language = _end_dc_language
  892.  
  893.     def _start_dc_publisher(self, attrsD):
  894.         self.push('publisher', 1)
  895.     _start_webmaster = _start_dc_publisher
  896.  
  897.     def _end_dc_publisher(self):
  898.         self.pop('publisher')
  899.         self._sync_author_detail('publisher')
  900.     _end_webmaster = _end_dc_publisher
  901.         
  902.     def _start_dcterms_issued(self, attrsD):
  903.         self.push('issued', 1)
  904.     _start_issued = _start_dcterms_issued
  905.  
  906.     def _end_dcterms_issued(self):
  907.         value = self.pop('issued')
  908.         self._save('issued_parsed', _parse_date(value))
  909.     _end_issued = _end_dcterms_issued
  910.  
  911.     def _start_dcterms_created(self, attrsD):
  912.         self.push('created', 1)
  913.     _start_created = _start_dcterms_created
  914.  
  915.     def _end_dcterms_created(self):
  916.         value = self.pop('created')
  917.         self._save('created_parsed', _parse_date(value))
  918.     _end_created = _end_dcterms_created
  919.  
  920.     def _start_dcterms_modified(self, attrsD):
  921.         self.push('modified', 1)
  922.     _start_modified = _start_dcterms_modified
  923.     _start_dc_date = _start_dcterms_modified
  924.     _start_pubdate = _start_dcterms_modified
  925.  
  926.     def _end_dcterms_modified(self):
  927.         value = self.pop('modified')
  928.         parsed_value = _parse_date(value)
  929.         self._save('modified_parsed', parsed_value)
  930.     _end_modified = _end_dcterms_modified
  931.     _end_dc_date = _end_dcterms_modified
  932.     _end_pubdate = _end_dcterms_modified
  933.  
  934.     def _start_expirationdate(self, attrsD):
  935.         self.push('expired', 1)
  936.  
  937.     def _end_expirationdate(self):
  938.         self._save('expired_parsed', _parse_date(self.pop('expired')))
  939.  
  940.     def _start_cc_license(self, attrsD):
  941.         self.push('license', 1)
  942.         value = self._getAttribute(attrsD, 'rdf:resource')
  943.         if value:
  944.             self.elementstack[-1][2].append(value)
  945.         self.pop('license')
  946.         
  947.     def _start_creativecommons_license(self, attrsD):
  948.         self.push('license', 1)
  949.  
  950.     def _end_creativecommons_license(self):
  951.         self.pop('license')
  952.  
  953.     def _start_category(self, attrsD):
  954.         self.push('category', 1)
  955.         domain = self._getAttribute(attrsD, 'domain')
  956.         cats = []
  957.         if self.inentry:
  958.             cats = self.entries[-1].setdefault('categories', [])
  959.         elif self.infeed:
  960.             cats = self.feeddata.setdefault('categories', [])
  961.         cats.append((domain, None))
  962.     _start_dc_subject = _start_category
  963.     _start_keywords = _start_category
  964.     _start_media_category = _start_category
  965.  
  966.     def _end_category(self):
  967.         self.pop('category')
  968.     _end_dc_subject = _end_category
  969.     _end_keywords = _end_category
  970.     _end_media_category = _end_category
  971.         
  972.     def _start_cloud(self, attrsD):
  973.         self.feeddata['cloud'] = FeedParserDict(attrsD)
  974.         
  975.     def _start_link(self, attrsD):
  976.         attrsD.setdefault('rel', 'alternate')
  977.         attrsD.setdefault('type', 'text/html')
  978.         if attrsD.has_key('href'):
  979.             attrsD['href'] = self.resolveURI(attrsD['href'])
  980.         expectingText = self.infeed or self.inentry
  981.         if self.inentry:
  982.             self.entries[-1].setdefault('links', [])
  983.             self.entries[-1]['links'].append(FeedParserDict(attrsD))
  984.         elif self.infeed:
  985.             self.feeddata.setdefault('links', [])
  986.             self.feeddata['links'].append(FeedParserDict(attrsD))
  987.         if attrsD.has_key('href'):
  988.             expectingText = 0
  989.             if attrsD.get('type', '') in self.html_types:
  990.                 if self.inentry:
  991.                     self.entries[-1]['link'] = attrsD['href']
  992.                 elif self.infeed:
  993.                     self.feeddata['link'] = attrsD['href']
  994.         else:
  995.             self.push('link', expectingText)
  996.     _start_producturl = _start_link
  997.  
  998.     def _end_link(self):
  999.         value = self.pop('link')
  1000.         if self.intextinput:
  1001.             context = self._getContext()
  1002.             context['textinput']['link'] = value
  1003.         if self.inimage:
  1004.             context = self._getContext()
  1005.             context['image']['link'] = value
  1006.     _end_producturl = _end_link
  1007.  
  1008.     def _start_guid(self, attrsD):
  1009.         self.guidislink = (attrsD.get('ispermalink', 'true') == 'true')
  1010.         self.push('id', 1)
  1011.  
  1012.     def _end_guid(self):
  1013.         value = self.pop('id')
  1014.         self._save('guidislink', self.guidislink and not self._getContext().has_key('link'))
  1015.         if self.guidislink:
  1016.             # guid acts as link, but only if "ispermalink" is not present or is "true",
  1017.             # and only if the item doesn't already have a link element
  1018.             self._save('link', value)
  1019.  
  1020.     def _start_id(self, attrsD):
  1021.         self.push('id', 1)
  1022.  
  1023.     def _end_id(self):
  1024.         value = self.pop('id')
  1025.             
  1026.     def _start_title(self, attrsD):
  1027.         self.incontent += 1
  1028.         if _debug: sys.stderr.write('attrsD.xml:lang = %s\n' % attrsD.get('xml:lang'))
  1029.         if _debug: sys.stderr.write('self.lang = %s\n' % self.lang)
  1030.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  1031.                               'type': attrsD.get('type', 'text/plain'),
  1032.                               'language': self.lang,
  1033.                               'base': self.baseuri})
  1034.         self.push('title', self.infeed or self.inentry)
  1035.     _start_dc_title = _start_title
  1036.  
  1037.     def _end_title(self):
  1038.         value = self.pop('title')
  1039.         self.incontent -= 1
  1040.         self.contentparams.clear()
  1041.         if self.intextinput:
  1042.             context = self._getContext()
  1043.             context['textinput']['title'] = value
  1044.         elif self.inimage:
  1045.             context = self._getContext()
  1046.             context['image']['title'] = value
  1047.     _end_dc_title = _end_title
  1048.  
  1049.     def _start_description(self, attrsD, default_content_type='text/html'):
  1050.         self.incontent += 1
  1051.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  1052.                               'type': attrsD.get('type', default_content_type),
  1053.                               'language': self.lang,
  1054.                               'base': self.baseuri})
  1055.         self.push('description', self.infeed or self.inentry)
  1056.  
  1057.     def _start_abstract(self, attrsD):
  1058.         return self._start_description(attrsD, 'text/plain')
  1059.  
  1060.     def _end_description(self):
  1061.         value = self.pop('description')
  1062.         self.incontent -= 1
  1063.         self.contentparams.clear()
  1064.         context = self._getContext()
  1065.         if self.intextinput:
  1066.             context['textinput']['description'] = value
  1067.         elif self.inimage:
  1068.             context['image']['description'] = value
  1069. #        elif self.inentry:
  1070. #            context['summary'] = value
  1071. #        elif self.infeed:
  1072. #            context['tagline'] = value
  1073.     _end_abstract = _end_description
  1074.  
  1075.     def _start_info(self, attrsD):
  1076.         self.incontent += 1
  1077.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  1078.                               'type': attrsD.get('type', 'text/plain'),
  1079.                               'language': self.lang,
  1080.                               'base': self.baseuri})
  1081.         self.push('info', 1)
  1082.  
  1083.     def _end_info(self):
  1084.         self.pop('info')
  1085.         self.incontent -= 1
  1086.         self.contentparams.clear()
  1087.  
  1088.     def _start_generator(self, attrsD):
  1089.         if attrsD:
  1090.             if attrsD.has_key('url'):
  1091.                 attrsD['url'] = self.resolveURI(attrsD['url'])
  1092.             self.feeddata['generator_detail'] = FeedParserDict(attrsD)
  1093.         self.push('generator', 1)
  1094.  
  1095.     def _end_generator(self):
  1096.         value = self.pop('generator')
  1097.         if self.feeddata.has_key('generator_detail'):
  1098.             self.feeddata['generator_detail']['name'] = value
  1099.             
  1100.     def _start_admin_generatoragent(self, attrsD):
  1101.         self.push('generator', 1)
  1102.         value = self._getAttribute(attrsD, 'rdf:resource')
  1103.         if value:
  1104.             self.elementstack[-1][2].append(value)
  1105.         self.pop('generator')
  1106.         self.feeddata['generator_detail'] = FeedParserDict({"url": value})
  1107.  
  1108.     def _start_admin_errorreportsto(self, attrsD):
  1109.         self.push('errorreportsto', 1)
  1110.         value = self._getAttribute(attrsD, 'rdf:resource')
  1111.         if value:
  1112.             self.elementstack[-1][2].append(value)
  1113.         self.pop('errorreportsto')
  1114.         
  1115.     def _start_summary(self, attrsD):
  1116.         self.incontent += 1
  1117.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'escaped'),
  1118.                               'type': attrsD.get('type', 'text/plain'),
  1119.                               'language': self.lang,
  1120.                               'base': self.baseuri})
  1121.         self.push('summary', 1)
  1122.  
  1123.     def _end_summary(self):
  1124.         value = self.pop('summary')
  1125.         if self.entries:
  1126.             self.entries[-1]['description'] = value
  1127.         self.incontent -= 1
  1128.         self.contentparams.clear()
  1129.         
  1130.     def _start_enclosure(self, attrsD):
  1131.         if self.inentry:
  1132.             self.inenclosure = 1
  1133.             self.entries[-1].setdefault('enclosures', [])
  1134.             self.entries[-1]['enclosures'].append(FeedParserDict(attrsD))
  1135.     _start_media_content = _start_enclosure            
  1136.  
  1137.     def _end_enclosure(self):
  1138.         self.inenclosure = 0
  1139.     _end_media_content = _end_enclosure
  1140.  
  1141.     def _start_media_thumbnail(self,attrsD):
  1142.         self.push('media:thumbnail',1)
  1143.         if self.inentry:
  1144.             if self.inenclosure:
  1145.                 self.entries[-1]['enclosures'][-1]['thumbnail']=FeedParserDict(attrsD)
  1146.             else:
  1147.                 self.entries[-1]['thumbnail'] = FeedParserDict(attrsD)
  1148.  
  1149.     def _end_media_thumbnail(self):
  1150.         self.pop('media:thumbnail')
  1151.         
  1152.     def _start_media_text(self,attrsD):
  1153.         self.push('media:text',1)
  1154.  
  1155.     def _end_media_text(self):
  1156.         value = self.pop('media:text')
  1157.         if self.inentry:
  1158.             if self.inenclosure:
  1159.                 self.entries[-1]['enclosures'][-1]['text'] = value
  1160.             else:
  1161.                 self.entries[-1]['text'] = value
  1162.  
  1163.     def _start_media_people(self,attrsD):
  1164.         self.push('media:people',1)
  1165.         try:
  1166.             self.peoplerole = attrsD['role']
  1167.         except:
  1168.             self.peoplerole = 'unknown'
  1169.  
  1170.     def _end_media_people(self):
  1171.         value = self.pop('media:people').split('|')
  1172.         if self.inentry:
  1173.             if self.inenclosure:
  1174.                 self.entries[-1]['enclosures'][-1].setdefault('roles', {})
  1175.                 self.entries[-1]['enclosures'][-1].roles[self.peoplerole]=value
  1176.             else:
  1177.                 self.entries[-1].setdefault('roles', {})
  1178.                 self.entries[-1].roles[self.peoplerole]=value
  1179.  
  1180.     def _start_dtv_startnback(self,attrsD):
  1181.         self.push('dtv:startnback',1)        
  1182.  
  1183.     def _end_dtv_startnback(self):
  1184.         self.feeddata['startnback'] = self.pop('dtv:startnback')
  1185.  
  1186.     def _start_dtv_librarylink(self,attrsD):
  1187.         self.push('dtv:librarylink',1)        
  1188.  
  1189.     def _end_dtv_librarylink(self):
  1190.         self.feeddata['librarylink'] = self.pop('dtv:librarylink')
  1191.  
  1192.     def _start_dtv_releasedate(self,attrsD):
  1193.         self.push('dtv:releasedate',1)        
  1194.  
  1195.     def _end_dtv_releasedate(self):
  1196.         value = self.pop('dtv:releasedate')
  1197.         if self.inentry:
  1198.             if self.inenclosure:
  1199.                 self.entries[-1]['enclosures'][-1]['releasedate'] = value
  1200.                 self.entries[-1]['enclosures'][-1]['releasedate_parsed'] = _parse_date(value)
  1201.             else:
  1202.                 self.entries[-1]['releasedate'] = value
  1203.                 self.entries[-1]['releasedate_parsed'] = _parse_date(value)
  1204.         
  1205.     def _start_dtv_paymentlink(self,attrsD):
  1206.         self.incontent += 1
  1207.         self.contentparams['mode'] = 'xml'
  1208.         self.contentparams['type'] = 'application/xhtml+xml'
  1209.         self.push('dtv:paymentlink',1)
  1210.         if self.inentry:
  1211.             if attrsD.has_key('url'):
  1212.                 if self.inenclosure:
  1213.                     self.entries[-1]['enclosures'][-1]['payment_url'] = attrsD['url']
  1214.                 else:
  1215.                     self.entries[-1]['payment_url'] = attrsD['url']
  1216.  
  1217.     def _end_dtv_paymentlink(self):
  1218.         value = _sanitizeHTML(self.pop('dtv:paymentlink'),self.encoding)
  1219.         self.incontent -= 1
  1220.         self.contentparams.clear()
  1221.         if self.inentry:
  1222.             if self.inenclosure:
  1223.                 self.entries[-1]['enclosures'][-1]['payment_html'] = value
  1224.             else:
  1225.                 self.entries[-1]['payment_html'] = value
  1226.         
  1227.  
  1228.     def _start_source(self, attrsD):
  1229.         if self.inentry:
  1230.             self.entries[-1]['source'] = FeedParserDict(attrsD)
  1231.         self.push('source', 1)
  1232.  
  1233.     def _end_source(self):
  1234.         self.pop('source')
  1235.  
  1236.     def _start_content(self, attrsD):
  1237.         self.incontent += 1
  1238.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'xml'),
  1239.                               'type': attrsD.get('type', 'text/plain'),
  1240.                               'language': self.lang,
  1241.                               'base': self.baseuri})
  1242.         self.push('content', 1)
  1243.  
  1244.     def _start_prodlink(self, attrsD):
  1245.         self.incontent += 1
  1246.         self.contentparams = FeedParserDict({'mode': attrsD.get('mode', 'xml'),
  1247.                               'type': attrsD.get('type', 'text/html'),
  1248.                               'language': self.lang,
  1249.                               'base': self.baseuri})
  1250.         self.push('content', 1)
  1251.  
  1252.     def _start_body(self, attrsD):
  1253.         self.incontent += 1
  1254.         self.contentparams = FeedParserDict({'mode': 'xml',
  1255.                               'type': 'application/xhtml+xml',
  1256.                               'language': self.lang,
  1257.                               'base': self.baseuri})
  1258.         self.push('content', 1)
  1259.     _start_xhtml_body = _start_body
  1260.  
  1261.     def _start_content_encoded(self, attrsD):
  1262.         self.incontent += 1
  1263.         self.contentparams = FeedParserDict({'mode': 'escaped',
  1264.                               'type': 'text/html',
  1265.                               'language': self.lang,
  1266.                               'base': self.baseuri})
  1267.         self.push('content', 1)
  1268.     _start_fullitem = _start_content_encoded
  1269.  
  1270.     def _end_content(self):
  1271.         value = self.pop('content')
  1272.         if self.contentparams.get('type') in (['text/plain'] + self.html_types):
  1273.             self._save('description', value)
  1274.         self.incontent -= 1
  1275.         self.contentparams.clear()
  1276.     _end_body = _end_content
  1277.     _end_xhtml_body = _end_content
  1278.     _end_content_encoded = _end_content
  1279.     _end_fullitem = _end_content
  1280.     _end_prodlink = _end_content
  1281.  
  1282. if _XML_AVAILABLE:
  1283.     class _StrictFeedParser(_FeedParserMixin, xml.sax.handler.ContentHandler):
  1284.         def __init__(self, baseuri, baselang, encoding):
  1285.             if _debug: sys.stderr.write('trying StrictFeedParser\n')
  1286.             xml.sax.handler.ContentHandler.__init__(self)
  1287.             _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
  1288.             self.bozo = 0
  1289.             self.exc = None
  1290.         
  1291.         def startPrefixMapping(self, prefix, uri):
  1292.             self.trackNamespace(prefix, uri)
  1293.         
  1294.         def startElementNS(self, name, qname, attrs):
  1295.             namespace, localname = name
  1296.             namespace = str(namespace or '')
  1297.             if namespace.find('backend.userland.com/rss') <> -1:
  1298.                 # match any backend.userland.com namespace
  1299.                 namespace = 'http://backend.userland.com/rss'
  1300.             prefix = self.namespaces.get(namespace, 'unknown')
  1301.             if prefix:
  1302.                 localname = prefix + ':' + localname
  1303.             localname = str(localname).lower()
  1304.  
  1305.             # qname implementation is horribly broken in Python 2.1 (it
  1306.             # doesn't report any), and slightly broken in Python 2.2 (it
  1307.             # doesn't report the xml: namespace). So we match up namespaces
  1308.             # with a known list first, and then possibly override them with
  1309.             # the qnames the SAX parser gives us (if indeed it gives us any
  1310.             # at all).  Thanks to MatejC for helping me test this and
  1311.             # tirelessly telling me that it didn't work yet.
  1312.             attrsD = {}
  1313.             for (namespace, attrlocalname), attrvalue in attrs._attrs.items():
  1314.                 prefix = self.namespaces.get(namespace, '')
  1315.                 if prefix:
  1316.                     attrlocalname = prefix + ":" + attrlocalname
  1317.                 attrsD[str(attrlocalname).lower()] = attrvalue
  1318.             for qname in attrs.getQNames():
  1319.                 attrsD[str(qname).lower()] = attrs.getValueByQName(qname)
  1320.             self.unknown_starttag(localname, attrsD.items())
  1321.  
  1322. #        def resolveEntity(self, publicId, systemId):
  1323. #            return _StringIO()
  1324.  
  1325.         def characters(self, text):
  1326.             self.handle_data(text)
  1327.  
  1328.         def endElementNS(self, name, qname):
  1329.             namespace, localname = name
  1330.             namespace = str(namespace)
  1331.             prefix = self.namespaces.get(namespace, '')
  1332.             if prefix:
  1333.                 localname = prefix + ':' + localname
  1334.             localname = str(localname).lower()
  1335.             self.unknown_endtag(localname)
  1336.  
  1337.         def error(self, exc):
  1338.             self.bozo = 1
  1339.             self.exc = exc
  1340.             
  1341.         def fatalError(self, exc):
  1342.             self.error(exc)
  1343.             raise exc
  1344.  
  1345. class _BaseHTMLProcessor(sgmllib.SGMLParser):
  1346.     elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr',
  1347.       'img', 'input', 'isindex', 'link', 'meta', 'param']
  1348.     
  1349.     def __init__(self, encoding):
  1350.         self.encoding = encoding
  1351.         if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding)
  1352.         sgmllib.SGMLParser.__init__(self)
  1353.         
  1354.     def reset(self):
  1355.         self.pieces = []
  1356.         sgmllib.SGMLParser.reset(self)
  1357.  
  1358.     def feed(self, data):
  1359.         data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'<!\1', data)
  1360.         data = re.sub(r'<(\S+)/>', r'<\1></\1>', data)
  1361.         data = data.replace(''', "'")
  1362.         data = data.replace('"', '"')
  1363.         if self.encoding and (type(data) == types.UnicodeType):
  1364.             data = data.encode(self.encoding)
  1365.         sgmllib.SGMLParser.feed(self, data)
  1366.  
  1367.     def normalize_attrs(self, attrs):
  1368.         # utility method to be called by descendants
  1369.         attrs = [(k.lower(), v) for k, v in attrs]
  1370. #        if self.encoding:
  1371. #            if _debug: sys.stderr.write('normalize_attrs, encoding=%s\n' % self.encoding)
  1372. #            attrs = [(k, v.encode(self.encoding)) for k, v in attrs]
  1373.         attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs]
  1374.         return attrs
  1375.  
  1376.     def unknown_starttag(self, tag, attrs):
  1377.         # called for each start tag
  1378.         # attrs is a list of (attr, value) tuples
  1379.         # e.g. for <pre class="screen">, tag="pre", attrs=[("class", "screen")]
  1380.         if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag)
  1381.         strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
  1382.         if tag in self.elements_no_end_tag:
  1383.             self.pieces.append("<%(tag)s%(strattrs)s />" % locals())
  1384.         else:
  1385.             self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
  1386.         
  1387.     def unknown_endtag(self, tag):
  1388.         # called for each end tag, e.g. for </pre>, tag will be "pre"
  1389.         # Reconstruct the original end tag.
  1390.         if tag not in self.elements_no_end_tag:
  1391.             self.pieces.append("</%(tag)s>" % locals())
  1392.  
  1393.     def handle_charref(self, ref):
  1394.         # called for each character reference, e.g. for " ", ref will be "160"
  1395.         # Reconstruct the original character reference.
  1396.         self.pieces.append("&#%(ref)s;" % locals())
  1397.         
  1398.     def handle_entityref(self, ref):
  1399.         # called for each entity reference, e.g. for "©", ref will be "copy"
  1400.         # Reconstruct the original entity reference.
  1401.         self.pieces.append("&%(ref)s;" % locals())
  1402.  
  1403.     def handle_data(self, text):
  1404.         # called for each block of plain text, i.e. outside of any tag and
  1405.         # not containing any character or entity references
  1406.         # Store the original text verbatim.
  1407.         if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text)
  1408.         self.pieces.append(text)
  1409.         
  1410.     def handle_comment(self, text):
  1411.         # called for each HTML comment, e.g. <!-- insert Javascript code here -->
  1412.         # Reconstruct the original comment.
  1413.         self.pieces.append("<!--%(text)s-->" % locals())
  1414.         
  1415.     def handle_pi(self, text):
  1416.         # called for each processing instruction, e.g. <?instruction>
  1417.         # Reconstruct original processing instruction.
  1418.         self.pieces.append("<?%(text)s>" % locals())
  1419.  
  1420.     def handle_decl(self, text):
  1421.         # called for the DOCTYPE, if present, e.g.
  1422.         # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  1423.         #     "http://www.w3.org/TR/html4/loose.dtd">
  1424.         # Reconstruct original DOCTYPE
  1425.         self.pieces.append("<!%(text)s>" % locals())
  1426.         
  1427.     _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
  1428.     def _scan_name(self, i, declstartpos):
  1429.         rawdata = self.rawdata
  1430.         n = len(rawdata)
  1431.         if i == n:
  1432.             return None, -1
  1433.         m = self._new_declname_match(rawdata, i)
  1434.         if m:
  1435.             s = m.group()
  1436.             name = s.strip()
  1437.             if (i + len(s)) == n:
  1438.                 return None, -1  # end of buffer
  1439.             return name.lower(), m.end()
  1440.         else:
  1441.             self.handle_data(rawdata)
  1442. #            self.updatepos(declstartpos, i)
  1443.             return None, -1
  1444.  
  1445.     def output(self):
  1446.         """Return processed HTML as a single string"""
  1447.         return "".join([str(p) for p in self.pieces])
  1448.  
  1449. class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
  1450.     def __init__(self, baseuri, baselang, encoding):
  1451.         sgmllib.SGMLParser.__init__(self)
  1452.         _FeedParserMixin.__init__(self, baseuri, baselang, encoding)
  1453.  
  1454.     def decodeEntities(self, element, data):
  1455.         data = data.replace('<', '<')
  1456.         data = data.replace('<', '<')
  1457.         data = data.replace('>', '>')
  1458.         data = data.replace('>', '>')
  1459.         data = data.replace('&', '&')
  1460.         data = data.replace('&', '&')
  1461.         data = data.replace('"', '"')
  1462.         data = data.replace('"', '"')
  1463.         data = data.replace(''', ''')
  1464.         data = data.replace(''', ''')
  1465.         if self.contentparams.get('mode') == 'escaped':
  1466.             data = data.replace('<', '<')
  1467.             data = data.replace('>', '>')
  1468.             data = data.replace('&', '&')
  1469.             data = data.replace('"', '"')
  1470.             data = data.replace(''', "'")
  1471.         return data
  1472.         
  1473. class _RelativeURIResolver(_BaseHTMLProcessor):
  1474.     relative_uris = [('a', 'href'),
  1475.                      ('applet', 'codebase'),
  1476.                      ('area', 'href'),
  1477.                      ('blockquote', 'cite'),
  1478.                      ('body', 'background'),
  1479.                      ('del', 'cite'),
  1480.                      ('form', 'action'),
  1481.                      ('frame', 'longdesc'),
  1482.                      ('frame', 'src'),
  1483.                      ('iframe', 'longdesc'),
  1484.                      ('iframe', 'src'),
  1485.                      ('head', 'profile'),
  1486.                      ('img', 'longdesc'),
  1487.                      ('img', 'src'),
  1488.                      ('img', 'usemap'),
  1489.                      ('input', 'src'),
  1490.                      ('input', 'usemap'),
  1491.                      ('ins', 'cite'),
  1492.                      ('link', 'href'),
  1493.                      ('object', 'classid'),
  1494.                      ('object', 'codebase'),
  1495.                      ('object', 'data'),
  1496.                      ('object', 'usemap'),
  1497.                      ('q', 'cite'),
  1498.                      ('script', 'src')]
  1499.  
  1500.     def __init__(self, baseuri, encoding):
  1501.         _BaseHTMLProcessor.__init__(self, encoding)
  1502.         self.baseuri = baseuri
  1503.  
  1504.     def resolveURI(self, uri):
  1505.         return urlparse.urljoin(self.baseuri, uri)
  1506.     
  1507.     def unknown_starttag(self, tag, attrs):
  1508.         attrs = self.normalize_attrs(attrs)
  1509.         attrs = [(key, ((tag, key) in self.relative_uris) and self.resolveURI(value) or value) for key, value in attrs]
  1510.         _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
  1511.         
  1512. def _resolveRelativeURIs(htmlSource, baseURI, encoding):
  1513.     if _debug: sys.stderr.write("entering _resolveRelativeURIs\n")
  1514.     p = _RelativeURIResolver(baseURI, encoding)
  1515.     p.feed(htmlSource)
  1516.     return p.output()
  1517.  
  1518. class _HTMLSanitizer(_BaseHTMLProcessor):
  1519.     acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
  1520.       'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col',
  1521.       'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset',
  1522.       'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
  1523.       'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
  1524.       'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small', 'span', 'strike',
  1525.       'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
  1526.       'thead', 'tr', 'tt', 'u', 'ul', 'var']
  1527.  
  1528.     acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
  1529.       'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing',
  1530.       'char', 'charoff', 'charset', 'checked', 'cite', 'class', 'clear', 'cols',
  1531.       'colspan', 'color', 'compact', 'coords', 'datetime', 'dir', 'disabled',
  1532.       'enctype', 'for', 'frame', 'headers', 'height', 'href', 'hreflang', 'hspace',
  1533.       'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'media', 'method',
  1534.       'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly',
  1535.       'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
  1536.       'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type',
  1537.       'usemap', 'valign', 'value', 'vspace', 'width']
  1538.  
  1539.     unacceptable_elements_with_end_tag = ['script', 'applet']
  1540.  
  1541.     def reset(self):
  1542.         _BaseHTMLProcessor.reset(self)
  1543.         self.unacceptablestack = 0
  1544.         
  1545.     def unknown_starttag(self, tag, attrs):
  1546.         if not tag in self.acceptable_elements:
  1547.             if tag in self.unacceptable_elements_with_end_tag:
  1548.                 self.unacceptablestack += 1
  1549.             return
  1550.         attrs = self.normalize_attrs(attrs)
  1551.         attrs = [(key, value) for key, value in attrs if key in self.acceptable_attributes]
  1552.         _BaseHTMLProcessor.unknown_starttag(self, tag, attrs)
  1553.         
  1554.     def unknown_endtag(self, tag):
  1555.         if not tag in self.acceptable_elements:
  1556.             if tag in self.unacceptable_elements_with_end_tag:
  1557.                 self.unacceptablestack -= 1
  1558.             return
  1559.         _BaseHTMLProcessor.unknown_endtag(self, tag)
  1560.  
  1561.     def handle_pi(self, text):
  1562.         pass
  1563.  
  1564.     def handle_decl(self, text):
  1565.         pass
  1566.  
  1567.     def handle_data(self, text):
  1568.         if not self.unacceptablestack:
  1569.             _BaseHTMLProcessor.handle_data(self, text)
  1570.  
  1571. def _sanitizeHTML(htmlSource, encoding):
  1572.     p = _HTMLSanitizer(encoding)
  1573.     p.feed(htmlSource)
  1574.     data = p.output()
  1575.     if _mxtidy and TIDY_MARKUP:
  1576.         nerrors, nwarnings, data, errordata = _mxtidy.tidy(data, output_xhtml=1, numeric_entities=1, wrap=0)
  1577.         if data.count('<body'):
  1578.             data = data.split('<body', 1)[1]
  1579.             if data.count('>'):
  1580.                 data = data.split('>', 1)[1]
  1581.         if data.count('</body'):
  1582.             data = data.split('</body', 1)[0]
  1583.     data = data.strip().replace('\r\n', '\n')
  1584.     return data
  1585.  
  1586. class _FeedURLHandler(urllib2.HTTPRedirectHandler, urllib2.HTTPDefaultErrorHandler):
  1587.     def http_error_default(self, req, fp, code, msg, headers):
  1588.         if ((code / 100) == 3) and (code != 304):
  1589.             return self.http_error_302(req, fp, code, msg, headers)
  1590.         infourl = urllib.addinfourl(fp, headers, req.get_full_url())
  1591.         infourl.status = code
  1592.         return infourl
  1593.  
  1594.     def http_error_302(self, req, fp, code, msg, headers):
  1595.         if headers.dict.has_key('location'):
  1596.             infourl = urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
  1597.         else:
  1598.             infourl = urllib.addinfourl(fp, headers, req.get_full_url())
  1599.         if not hasattr(infourl, 'status'):
  1600.             infourl.status = code
  1601.         return infourl
  1602.  
  1603.     def http_error_301(self, req, fp, code, msg, headers):
  1604.         if headers.dict.has_key('location'):
  1605.             infourl = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers)
  1606.         else:
  1607.             infourl = urllib.addinfourl(fp, headers, req.get_full_url())
  1608.         if not hasattr(infourl, 'status'):
  1609.             infourl.status = code
  1610.         return infourl
  1611.  
  1612.     http_error_300 = http_error_302
  1613.     http_error_303 = http_error_302
  1614.     http_error_307 = http_error_302
  1615.         
  1616. def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers):
  1617.     """URL, filename, or string --> stream
  1618.  
  1619.     This function lets you define parsers that take any input source
  1620.     (URL, pathname to local or network file, or actual data as a string)
  1621.     and deal with it in a uniform manner.  Returned object is guaranteed
  1622.     to have all the basic stdio read methods (read, readline, readlines).
  1623.     Just .close() the object when you're done with it.
  1624.  
  1625.     If the etag argument is supplied, it will be used as the value of an
  1626.     If-None-Match request header.
  1627.  
  1628.     If the modified argument is supplied, it must be a tuple of 9 integers
  1629.     as returned by gmtime() in the standard Python time module. This MUST
  1630.     be in GMT (Greenwich Mean Time). The formatted date/time will be used
  1631.     as the value of an If-Modified-Since request header.
  1632.  
  1633.     If the agent argument is supplied, it will be used as the value of a
  1634.     User-Agent request header.
  1635.  
  1636.     If the referrer argument is supplied, it will be used as the value of a
  1637.     Referer[sic] request header.
  1638.  
  1639.     If handlers is supplied, it is a list of handlers used to build a
  1640.     urllib2 opener.
  1641.     """
  1642.  
  1643.     if hasattr(url_file_stream_or_string, "read"):
  1644.         return url_file_stream_or_string
  1645.  
  1646.     if url_file_stream_or_string == "-":
  1647.         return sys.stdin
  1648.  
  1649.     if urlparse.urlparse(url_file_stream_or_string)[0] in ('http', 'https', 'ftp'):
  1650.         if not agent:
  1651.             agent = USER_AGENT
  1652.         # test for inline user:password for basic auth
  1653.         auth = None
  1654.         if base64:
  1655.             urltype, rest = urllib.splittype(url_file_stream_or_string)
  1656.             realhost, rest = urllib.splithost(rest)
  1657.             if realhost:
  1658.                 user_passwd, realhost = urllib.splituser(realhost)
  1659.                 if user_passwd:
  1660.                     url_file_stream_or_string = "%s://%s%s" % (urltype, realhost, rest)
  1661.                     auth = base64.encodestring(user_passwd).strip()
  1662.         # try to open with urllib2 (to use optional headers)
  1663.         request = urllib2.Request(url_file_stream_or_string)
  1664.         request.add_header("User-Agent", agent)
  1665.         if etag:
  1666.             request.add_header("If-None-Match", etag)
  1667.         if modified:
  1668.             # format into an RFC 1123-compliant timestamp. We can't use
  1669.             # time.strftime() since the %a and %b directives can be affected
  1670.             # by the current locale, but RFC 2616 states that dates must be
  1671.             # in English.
  1672.             short_weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
  1673.             months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  1674.             request.add_header("If-Modified-Since", "%s, %02d %s %04d %02d:%02d:%02d GMT" % (short_weekdays[modified[6]], modified[2], months[modified[1] - 1], modified[0], modified[3], modified[4], modified[5]))
  1675.         if referrer:
  1676.             request.add_header("Referer", referrer)
  1677.         if gzip and zlib:
  1678.             request.add_header("Accept-encoding", "gzip, deflate")
  1679.         elif gzip:
  1680.             request.add_header("Accept-encoding", "gzip")
  1681.         elif zlib:
  1682.             request.add_header("Accept-encoding", "deflate")
  1683.         else:
  1684.             request.add_header("Accept-encoding", "")
  1685.         if auth:
  1686.             request.add_header("Authorization", "Basic %s" % auth)
  1687.         if ACCEPT_HEADER:
  1688.             request.add_header("Accept", ACCEPT_HEADER)
  1689.         opener = apply(urllib2.build_opener, tuple([_FeedURLHandler()] + handlers))
  1690.         opener.addheaders = [] # RMK - must clear so we only send our custom User-Agent
  1691.         try:
  1692.             return opener.open(request)
  1693.         finally:
  1694.             opener.close() # JohnD
  1695.     
  1696.     # try to open with native open function (if url_file_stream_or_string is a filename)
  1697.     try:
  1698.         return open(url_file_stream_or_string)
  1699.     except:
  1700.         pass
  1701.  
  1702.     # treat url_file_stream_or_string as string
  1703.     return _StringIO(str(url_file_stream_or_string))
  1704.  
  1705. _date_handlers = []
  1706. def registerDateHandler(func):
  1707.     """Register a date handler function (takes string, returns 9-tuple date in GMT)"""
  1708.     _date_handlers.insert(0, func)
  1709.     
  1710. # ISO-8601 date parsing routines written by Fazal Majid.
  1711. # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601
  1712. # parser is beyond the scope of feedparser and would be a worthwhile addition
  1713. # to the Python library.
  1714. # A single regular expression cannot parse ISO 8601 date formats into groups
  1715. # as the standard is highly irregular (for instance is 030104 2003-01-04 or
  1716. # 0301-04-01), so we use templates instead.
  1717. # Please note the order in templates is significant because we need a
  1718. # greedy match.
  1719. _iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-MM', 'YYYY-?OOO',
  1720.                 'YY-?MM-?DD', 'YY-?OOO', 'YYYY', 
  1721.                 '-YY-?MM', '-OOO', '-YY',
  1722.                 '--MM-?DD', '--MM',
  1723.                 '---DD',
  1724.                 'CC', '']
  1725. _iso8601_re = [
  1726.     tmpl.replace(
  1727.     'YYYY', r'(?P<year>\d{4})').replace(
  1728.     'YY', r'(?P<year>\d\d)').replace(
  1729.     'MM', r'(?P<month>[01]\d)').replace(
  1730.     'DD', r'(?P<day>[0123]\d)').replace(
  1731.     'OOO', r'(?P<ordinal>[0123]\d\d)').replace(
  1732.     'CC', r'(?P<century>\d\d$)')
  1733.     + r'(T?(?P<hour>\d{2}):(?P<minute>\d{2})'
  1734.     + r'(:(?P<second>\d{2}))?'
  1735.     + r'(?P<tz>[+-](?P<tzhour>\d{2})(:(?P<tzmin>\d{2}))?|Z)?)?'
  1736.     for tmpl in _iso8601_tmpl]
  1737. del tmpl
  1738. _iso8601_matches = [re.compile(regex).match for regex in _iso8601_re]
  1739. del regex
  1740. def _parse_date_iso8601(dateString):
  1741.     """Parse a variety of ISO-8601-compatible formats like 20040105"""
  1742.     m = None
  1743.     for _iso8601_match in _iso8601_matches:
  1744.         m = _iso8601_match(dateString)
  1745.         if m: break
  1746.     if not m: return
  1747.     if m.span() == (0, 0): return
  1748.     params = m.groupdict()
  1749.     ordinal = params.get("ordinal", 0)
  1750.     if ordinal:
  1751.         ordinal = int(ordinal)
  1752.     else:
  1753.         ordinal = 0
  1754.     year = params.get("year", "--")
  1755.     if not year or year == "--":
  1756.         year = time.gmtime()[0]
  1757.     elif len(year) == 2:
  1758.         # ISO 8601 assumes current century, i.e. 93 -> 2093, NOT 1993
  1759.         year = 100 * int(time.gmtime()[0] / 100) + int(year)
  1760.     else:
  1761.         year = int(year)
  1762.     month = params.get("month", "-")
  1763.     if not month or month == "-":
  1764.         # ordinals are NOT normalized by mktime, we simulate them
  1765.         # by setting month=1, day=ordinal
  1766.         if ordinal:
  1767.             month = 1
  1768.         else:
  1769.             month = time.gmtime()[1]
  1770.     month = int(month)
  1771.     day = params.get("day", 0)
  1772.     if not day:
  1773.         # see above
  1774.         if ordinal:
  1775.             day = ordinal
  1776.         elif params.get("century", 0) or \
  1777.                  params.get("year", 0) or params.get("month", 0):
  1778.             day = 1
  1779.         else:
  1780.             day = time.gmtime()[2]
  1781.     else:
  1782.         day = int(day)
  1783.     # special case of the century - is the first year of the 21st century
  1784.     # 2000 or 2001 ? The debate goes on...
  1785.     if "century" in params.keys():
  1786.         year = (int(params["century"]) - 1) * 100 + 1
  1787.     # in ISO 8601 most fields are optional
  1788.     for field in ["hour", "minute", "second", "tzhour", "tzmin"]:
  1789.         if not params.get(field, None):
  1790.             params[field] = 0
  1791.     hour = int(params.get("hour", 0))
  1792.     minute = int(params.get("minute", 0))
  1793.     second = int(params.get("second", 0))
  1794.     # weekday is normalized by mktime(), we can ignore it
  1795.     weekday = 0
  1796.     # daylight savings is complex, but not needed for feedparser's purposes
  1797.     # as time zones, if specified, include mention of whether it is active
  1798.     # (e.g. PST vs. PDT, CET). Using -1 is implementation-dependent and
  1799.     # and most implementations have DST bugs
  1800.     daylight_savings_flag = 0
  1801.     tm = [year, month, day, hour, minute, second, weekday,
  1802.           ordinal, daylight_savings_flag]
  1803.     # ISO 8601 time zone adjustments
  1804.     tz = params.get("tz")
  1805.     if tz and tz != "Z":
  1806.         if tz[0] == "-":
  1807.             tm[3] += int(params.get("tzhour", 0))
  1808.             tm[4] += int(params.get("tzmin", 0))
  1809.         elif tz[0] == "+":
  1810.             tm[3] -= int(params.get("tzhour", 0))
  1811.             tm[4] -= int(params.get("tzmin", 0))
  1812.         else:
  1813.             return None
  1814.     # Python's time.mktime() is a wrapper around the ANSI C mktime(3c)
  1815.     # which is guaranteed to normalize d/m/y/h/m/s.
  1816.     # Many implementations have bugs, but we'll pretend they don't.
  1817.     return time.localtime(time.mktime(tm))
  1818. registerDateHandler(_parse_date_iso8601)
  1819.     
  1820. # 8-bit date handling routines written by ytrewq1.
  1821. _korean_year  = u'\ub144' # b3e2 in euc-kr
  1822. _korean_month = u'\uc6d4' # bff9 in euc-kr
  1823. _korean_day   = u'\uc77c' # c0cf in euc-kr
  1824. _korean_am    = u'\uc624\uc804' # bfc0 c0fc in euc-kr
  1825. _korean_pm    = u'\uc624\ud6c4' # bfc0 c8c4 in euc-kr
  1826.  
  1827. _korean_onblog_date_re = \
  1828.     re.compile('(\d{4})%s\s+(\d{2})%s\s+(\d{2})%s\s+(\d{2}):(\d{2}):(\d{2})' % \
  1829.                (_korean_year, _korean_month, _korean_day))
  1830. _korean_nate_date_re = \
  1831.     re.compile(u'(\d{4})-(\d{2})-(\d{2})\s+(%s|%s)\s+(\d{,2}):(\d{,2}):(\d{,2})' % \
  1832.                (_korean_am, _korean_pm))
  1833. def _parse_date_onblog(dateString):
  1834.     """Parse a string according to the OnBlog 8-bit date format"""
  1835.     m = _korean_onblog_date_re.match(dateString)
  1836.     if not m: return
  1837.     w3dtfdate = "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s" % \
  1838.                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
  1839.                  'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
  1840.                  'zonediff': '+09:00'}
  1841.     if _debug: sys.stderr.write("OnBlog date parsed as: %s\n" % w3dtfdate)
  1842.     return _parse_date_w3dtf(w3dtfdate)
  1843. registerDateHandler(_parse_date_onblog)
  1844.  
  1845. def _parse_date_nate(dateString):
  1846.     """Parse a string according to the Nate 8-bit date format"""
  1847.     m = _korean_nate_date_re.match(dateString)
  1848.     if not m: return
  1849.     hour = int(m.group(5))
  1850.     ampm = m.group(4)
  1851.     if (ampm == _korean_pm):
  1852.         hour += 12
  1853.     hour = str(hour)
  1854.     if len(hour) == 1:
  1855.         hour = '0' + hour
  1856.     w3dtfdate = "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s" % \
  1857.                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
  1858.                  'hour': hour, 'minute': m.group(6), 'second': m.group(7),\
  1859.                  'zonediff': '+09:00'}
  1860.     if _debug: sys.stderr.write("Nate date parsed as: %s\n" % w3dtfdate)
  1861.     return _parse_date_w3dtf(w3dtfdate)
  1862. registerDateHandler(_parse_date_nate)
  1863.  
  1864. _mssql_date_re = \
  1865.     re.compile('(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\.\d+')
  1866. def _parse_date_mssql(dateString):
  1867.     """Parse a string according to the MS SQL date format"""
  1868.     m = _mssql_date_re.match(dateString)
  1869.     if not m: return
  1870.     w3dtfdate = "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s:%(second)s%(zonediff)s" % \
  1871.                 {'year': m.group(1), 'month': m.group(2), 'day': m.group(3),\
  1872.                  'hour': m.group(4), 'minute': m.group(5), 'second': m.group(6),\
  1873.                  'zonediff': '+09:00'}
  1874.     if _debug: sys.stderr.write("MS SQL date parsed as: %s\n" % w3dtfdate)
  1875.     return _parse_date_w3dtf(w3dtfdate)
  1876. registerDateHandler(_parse_date_mssql)
  1877.  
  1878. # Unicode strings for Greek date strings
  1879. _greek_months = \
  1880.   { \
  1881.    u'\u0399\u03b1\u03bd': u'Jan',       # c9e1ed in iso-8859-7
  1882.    u'\u03a6\u03b5\u03b2': u'Feb',       # d6e5e2 in iso-8859-7
  1883.    u'\u039c\u03ac\u03ce': u'Mar',       # ccdcfe in iso-8859-7
  1884.    u'\u039c\u03b1\u03ce': u'Mar',       # cce1fe in iso-8859-7
  1885.    u'\u0391\u03c0\u03c1': u'Apr',       # c1f0f1 in iso-8859-7
  1886.    u'\u039c\u03ac\u03b9': u'May',       # ccdce9 in iso-8859-7
  1887.    u'\u039c\u03b1\u03ca': u'May',       # cce1fa in iso-8859-7
  1888.    u'\u039c\u03b1\u03b9': u'May',       # cce1e9 in iso-8859-7
  1889.    u'\u0399\u03bf\u03cd\u03bd': u'Jun', # c9effded in iso-8859-7
  1890.    u'\u0399\u03bf\u03bd': u'Jun',       # c9efed in iso-8859-7
  1891.    u'\u0399\u03bf\u03cd\u03bb': u'Jul', # c9effdeb in iso-8859-7
  1892.    u'\u0399\u03bf\u03bb': u'Jul',       # c9f9eb in iso-8859-7
  1893.    u'\u0391\u03cd\u03b3': u'Aug',       # c1fde3 in iso-8859-7
  1894.    u'\u0391\u03c5\u03b3': u'Aug',       # c1f5e3 in iso-8859-7
  1895.    u'\u03a3\u03b5\u03c0': u'Sep',       # d3e5f0 in iso-8859-7
  1896.    u'\u039f\u03ba\u03c4': u'Oct',       # cfeaf4 in iso-8859-7
  1897.    u'\u039d\u03bf\u03ad': u'Nov',       # cdefdd in iso-8859-7
  1898.    u'\u039d\u03bf\u03b5': u'Nov',       # cdefe5 in iso-8859-7
  1899.    u'\u0394\u03b5\u03ba': u'Dec',       # c4e5ea in iso-8859-7
  1900.   }
  1901.  
  1902. _greek_wdays = \
  1903.   { \
  1904.    u'\u039a\u03c5\u03c1': u'Sun', # caf5f1 in iso-8859-7
  1905.    u'\u0394\u03b5\u03c5': u'Mon', # c4e5f5 in iso-8859-7
  1906.    u'\u03a4\u03c1\u03b9': u'Tue', # d4f1e9 in iso-8859-7
  1907.    u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7
  1908.    u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7
  1909.    u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7
  1910.    u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7   
  1911.   }
  1912.  
  1913. _greek_date_format_re = \
  1914.     re.compile(u'([^,]+),\s+(\d{2})\s+([^\s]+)\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+([^\s]+)')
  1915.  
  1916. def _parse_date_greek(dateString):
  1917.     """Parse a string according to a Greek 8-bit date format."""
  1918.     m = _greek_date_format_re.match(dateString)
  1919.     if not m: return
  1920.     try:
  1921.         wday = _greek_wdays[m.group(1)]
  1922.         month = _greek_months[m.group(3)]
  1923.     except:
  1924.         return
  1925.     rfc822date = "%(wday)s, %(day)s %(month)s %(year)s %(hour)s:%(minute)s:%(second)s %(zonediff)s" % \
  1926.                  {'wday': wday, 'day': m.group(2), 'month': month, 'year': m.group(4),\
  1927.                   'hour': m.group(5), 'minute': m.group(6), 'second': m.group(7),\
  1928.                   'zonediff': m.group(8)}
  1929.     if _debug: sys.stderr.write("Greek date parsed as: %s\n" % rfc822date)
  1930.     return _parse_date_rfc822(rfc822date)
  1931. registerDateHandler(_parse_date_greek)
  1932.  
  1933. # Unicode strings for Hungarian date strings
  1934. _hungarian_months = \
  1935.   { \
  1936.     u'janu\u00e1r':   u'01',  # e1 in iso-8859-2
  1937.     u'febru\u00e1ri': u'02',  # e1 in iso-8859-2
  1938.     u'm\u00e1rcius':  u'03',  # e1 in iso-8859-2
  1939.     u'\u00e1prilis':  u'04',  # e1 in iso-8859-2
  1940.     u'm\u00e1ujus':   u'05',  # e1 in iso-8859-2
  1941.     u'j\u00fanius':   u'06',  # fa in iso-8859-2
  1942.     u'j\u00falius':   u'07',  # fa in iso-8859-2
  1943.     u'augusztus':     u'08',
  1944.     u'szeptember':    u'09',
  1945.     u'okt\u00f3ber':  u'10',  # f3 in iso-8859-2
  1946.     u'november':      u'11',
  1947.     u'december':      u'12',
  1948.   }
  1949.  
  1950. _hungarian_date_format_re = \
  1951.   re.compile(u'(\d{4})-([^-]+)-(\d{,2})T(\d{,2}):(\d{2})((\+|-)(\d{,2}:\d{2}))')
  1952.  
  1953. def _parse_date_hungarian(dateString):
  1954.     """Parse a string according to a Hungarian 8-bit date format."""
  1955.     m = _hungarian_date_format_re.match(dateString)
  1956.     if not m: return
  1957.     try:
  1958.         month = _hungarian_months[m.group(2)]
  1959.         day = m.group(3)
  1960.         if len(day) == 1:
  1961.             day = '0' + day
  1962.         hour = m.group(4)
  1963.         if len(hour) == 1:
  1964.             hour = '0' + hour
  1965.     except:
  1966.         return
  1967.     w3dtfdate = "%(year)s-%(month)s-%(day)sT%(hour)s:%(minute)s%(zonediff)s" % \
  1968.                 {'year': m.group(1), 'month': month, 'day': day,\
  1969.                  'hour': hour, 'minute': m.group(5),\
  1970.                  'zonediff': m.group(6)}
  1971.     if _debug: sys.stderr.write("Hungarian date parsed as: %s\n" % w3dtfdate)
  1972.     return _parse_date_w3dtf(w3dtfdate)
  1973. registerDateHandler(_parse_date_hungarian)
  1974.  
  1975. # W3DTF-style date parsing adapted from PyXML xml.utils.iso8601, written by
  1976. # Drake and licensed under the Python license.  Removed all range checking
  1977. # for month, day, hour, minute, and second, since mktime will normalize
  1978. # these later
  1979. def _parse_date_w3dtf(dateString):
  1980.     def __extract_date(m):
  1981.         year = int(m.group("year"))
  1982.         if year < 100:
  1983.             year = 100 * int(time.gmtime()[0] / 100) + int(year)
  1984.         if year < 1000:
  1985.             return 0, 0, 0
  1986.         julian = m.group("julian")
  1987.         if julian:
  1988.             julian = int(julian)
  1989.             month = julian / 30 + 1
  1990.             day = julian % 30 + 1
  1991.             jday = None
  1992.             while jday != julian:
  1993.                 t = time.mktime((year, month, day, 0, 0, 0, 0, 0, 0))
  1994.                 jday = time.gmtime(t)[-2]
  1995.                 diff = abs(jday - julian)
  1996.                 if jday > julian:
  1997.                     if diff < day:
  1998.                         day = day - diff
  1999.                     else:
  2000.                         month = month - 1
  2001.                         day = 31
  2002.                 elif jday < julian:
  2003.                     if day + diff < 28:
  2004.                        day = day + diff
  2005.                     else:
  2006.                         month = month + 1
  2007.             return year, month, day
  2008.         month = m.group("month")
  2009.         day = 1
  2010.         if month is None:
  2011.             month = 1
  2012.         else:
  2013.             month = int(month)
  2014.             day = m.group("day")
  2015.             if day:
  2016.                 day = int(day)
  2017.             else:
  2018.                 day = 1
  2019.         return year, month, day
  2020.  
  2021.     def __extract_time(m):
  2022.         if not m:
  2023.             return 0, 0, 0
  2024.         hours = m.group("hours")
  2025.         if not hours:
  2026.             return 0, 0, 0
  2027.         hours = int(hours)
  2028.         minutes = int(m.group("minutes"))
  2029.         seconds = m.group("seconds")
  2030.         if seconds:
  2031.             seconds = int(seconds)
  2032.         else:
  2033.             seconds = 0
  2034.         return hours, minutes, seconds
  2035.  
  2036.     def __extract_tzd(m):
  2037.         """Return the Time Zone Designator as an offset in seconds from UTC."""
  2038.         if not m:
  2039.             return 0
  2040.         tzd = m.group("tzd")
  2041.         if not tzd:
  2042.             return 0
  2043.         if tzd == "Z":
  2044.             return 0
  2045.         hours = int(m.group("tzdhours"))
  2046.         minutes = m.group("tzdminutes")
  2047.         if minutes:
  2048.             minutes = int(minutes)
  2049.         else:
  2050.             minutes = 0
  2051.         offset = (hours*60 + minutes) * 60
  2052.         if tzd[0] == "+":
  2053.             return -offset
  2054.         return offset
  2055.  
  2056.     __date_re = ("(?P<year>\d\d\d\d)"
  2057.                  "(?:(?P<dsep>-|)"
  2058.                  "(?:(?P<julian>\d\d\d)"
  2059.                  "|(?P<month>\d\d)(?:(?P=dsep)(?P<day>\d\d))?))?")
  2060.     __tzd_re = "(?P<tzd>[-+](?P<tzdhours>\d\d)(?::?(?P<tzdminutes>\d\d))|Z)"
  2061.     __tzd_rx = re.compile(__tzd_re)
  2062.     __time_re = ("(?P<hours>\d\d)(?P<tsep>:|)(?P<minutes>\d\d)"
  2063.                  "(?:(?P=tsep)(?P<seconds>\d\d(?:[.,]\d+)?))?"
  2064.                  + __tzd_re)
  2065.     __datetime_re = "%s(?:T%s)?" % (__date_re, __time_re)
  2066.     __datetime_rx = re.compile(__datetime_re)
  2067.     m = __datetime_rx.match(dateString)
  2068.     if (m is None) or (m.group() != dateString): return
  2069.     gmt = __extract_date(m) + __extract_time(m) + (0, 0, 0)
  2070.     if gmt[0] == 0: return
  2071.     return time.gmtime(time.mktime(gmt) + __extract_tzd(m) - time.timezone)
  2072. registerDateHandler(_parse_date_w3dtf)
  2073.  
  2074. def _parse_date_rfc822(dateString):
  2075.     """Parse an RFC822, RFC1123, RFC2822, or asctime-style date"""
  2076.     tm = rfc822.parsedate_tz(dateString)
  2077.     if tm:
  2078.         return time.gmtime(rfc822.mktime_tz(tm))
  2079. # rfc822.py defines several time zones, but we define some extra ones.
  2080. # "ET" is equivalent to "EST", etc.
  2081. _additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
  2082. rfc822._timezones.update(_additional_timezones)
  2083. registerDateHandler(_parse_date_rfc822)    
  2084.  
  2085. def _parse_date(dateString):
  2086.     """Parses a variety of date formats into a 9-tuple in GMT"""
  2087.     for handler in _date_handlers:
  2088.         try:
  2089.             date9tuple = handler(dateString)
  2090.             if not date9tuple: continue
  2091.             if len(date9tuple) != 9:
  2092.                 if _debug: sys.stderr.write("date handler function must return 9-tuple\n")
  2093.                 raise ValueError
  2094.             map(int, date9tuple)
  2095.             return date9tuple
  2096.         except Exception, e:
  2097.             if _debug: sys.stderr.write("%s raised %s\n" % (handler.__name__, repr(e)))
  2098.             pass
  2099.     return None
  2100.  
  2101. def _getCharacterEncoding(http_headers, xml_data):
  2102.     """Get the character encoding of the XML document
  2103.  
  2104.     http_headers is a dictionary
  2105.     xml_data is a raw string (not Unicode)
  2106.     
  2107.     This is so much trickier than it sounds, it's not even funny.
  2108.     According to RFC 3023 ("XML Media Types"), if the HTTP Content-Type
  2109.     is application/xml, application/*+xml,
  2110.     application/xml-external-parsed-entity, or application/xml-dtd,
  2111.     the encoding given in the charset parameter of the HTTP Content-Type
  2112.     takes precedence over the encoding given in the XML prefix within the
  2113.     document, and defaults to "utf-8" if neither are specified.  But, if
  2114.     the HTTP Content-Type is text/xml, text/*+xml, or
  2115.     text/xml-external-parsed-entity, the encoding given in the XML prefix
  2116.     within the document is ALWAYS IGNORED and only the encoding given in
  2117.     the charset parameter of the HTTP Content-Type header should be
  2118.     respected, and it defaults to "us-ascii" if not specified.
  2119.  
  2120.     Furthermore, discussion on the atom-syntax mailing list with the
  2121.     author of RFC 3023 leads me to the conclusion that any document
  2122.     served with a Content-Type of text/* and no charset parameter
  2123.     must be treated as us-ascii.  (We now do this.)  And also that it
  2124.     must always be flagged as non-well-formed.  (We now do this too.)
  2125.     
  2126.     If Content-Type is unspecified (input was local file or non-HTTP source)
  2127.     or unrecognized (server just got it totally wrong), then go by the
  2128.     encoding given in the XML prefix of the document and default to
  2129.     "iso-8859-1" as per the HTTP specification (RFC 2616).
  2130.     
  2131.     Then, assuming we didn't find a character encoding in the HTTP headers
  2132.     (and the HTTP Content-type allowed us to look in the body), we need
  2133.     to sniff the first few bytes of the XML data and try to determine
  2134.     whether the encoding is ASCII-compatible.  Section F of the XML
  2135.     specification shows the way here:
  2136.     http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
  2137.  
  2138.     If the sniffed encoding is not ASCII-compatible, we need to make it
  2139.     ASCII compatible so that we can sniff further into the XML declaration
  2140.     to find the encoding attribute, which will tell us the true encoding.
  2141.  
  2142.     Of course, none of this guarantees that we will be able to parse the
  2143.     feed in the declared character encoding (assuming it was declared
  2144.     correctly, which many are not).  CJKCodecs and iconv_codec help a lot;
  2145.     you should definitely install them if you can.
  2146.     http://cjkpython.i18n.org/
  2147.     """
  2148.  
  2149.     def _parseHTTPContentType(content_type):
  2150.         """takes HTTP Content-Type header and returns (content type, charset)
  2151.  
  2152.         If no charset is specified, returns (content type, '')
  2153.         If no content type is specified, returns ('', '')
  2154.         Both return parameters are guaranteed to be lowercase strings
  2155.         """
  2156.         content_type = content_type or ''
  2157.         content_type, params = cgi.parse_header(content_type)
  2158.         return content_type, params.get('charset', '').replace("'", "")
  2159.  
  2160.     sniffed_xml_encoding = ''
  2161.     xml_encoding = ''
  2162.     true_encoding = ''
  2163.     http_content_type, http_encoding = _parseHTTPContentType(http_headers.get("content-type"))
  2164.     # Must sniff for non-ASCII-compatible character encodings before
  2165.     # searching for XML declaration.  This heuristic is defined in
  2166.     # section F of the XML specification:
  2167.     # http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info
  2168.     try:
  2169.         if xml_data[:4] == '\x4c\x6f\xa7\x94':
  2170.             # EBCDIC
  2171.             xml_data = _ebcdic_to_ascii(xml_data)
  2172.         elif xml_data[:4] == '\x00\x3c\x00\x3f':
  2173.             # UTF-16BE
  2174.             sniffed_xml_encoding = 'utf-16be'
  2175.             xml_data = unicode(xml_data, 'utf-16be').encode('utf-8')
  2176.         elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') and (xml_data[2:4] != '\x00\x00'):
  2177.             # UTF-16BE with BOM
  2178.             sniffed_xml_encoding = 'utf-16be'
  2179.             xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8')
  2180.         elif xml_data[:4] == '\x3c\x00\x3f\x00':
  2181.             # UTF-16LE
  2182.             sniffed_xml_encoding = 'utf-16le'
  2183.             xml_data = unicode(xml_data, 'utf-16le').encode('utf-8')
  2184.         elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and (xml_data[2:4] != '\x00\x00'):
  2185.             # UTF-16LE with BOM
  2186.             sniffed_xml_encoding = 'utf-16le'
  2187.             xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8')
  2188.         elif xml_data[:4] == '\x00\x00\x00\x3c':
  2189.             # UTF-32BE
  2190.             sniffed_xml_encoding = 'utf-32be'
  2191.             xml_data = unicode(xml_data, 'utf-32be').encode('utf-8')
  2192.         elif xml_data[:4] == '\x3c\x00\x00\x00':
  2193.             # UTF-32LE
  2194.             sniffed_xml_encoding = 'utf-32le'
  2195.             xml_data = unicode(xml_data, 'utf-32le').encode('utf-8')
  2196.         elif xml_data[:4] == '\x00\x00\xfe\xff':
  2197.             # UTF-32BE with BOM
  2198.             sniffed_xml_encoding = 'utf-32be'
  2199.             xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8')
  2200.         elif xml_data[:4] == '\xff\xfe\x00\x00':
  2201.             # UTF-32LE with BOM
  2202.             sniffed_xml_encoding = 'utf-32le'
  2203.             xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8')
  2204.         elif xml_data[:3] == '\xef\xbb\xbf':
  2205.             # UTF-8 with BOM
  2206.             sniffed_xml_encoding = 'utf-8'
  2207.             xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8')
  2208.         else:
  2209.             # ASCII-compatible
  2210.             pass
  2211.         xml_encoding_match = re.compile('^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data)
  2212.     except:
  2213.         xml_encoding_match = None
  2214.     if xml_encoding_match:
  2215.         xml_encoding = xml_encoding_match.groups()[0].lower()
  2216.         if sniffed_xml_encoding and (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', 'iso-10646-ucs-4', 'ucs-4', 'csucs4', 'utf-16', 'utf-32', 'utf_16', 'utf_32', 'utf16', 'u16')):
  2217.             xml_encoding = sniffed_xml_encoding
  2218.     acceptable_content_type = 0
  2219.     application_content_types = ('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity')
  2220.     text_content_types = ('text/xml', 'text/xml-external-parsed-entity')
  2221.     if (http_content_type in application_content_types) or \
  2222.        (http_content_type.startswith('application/') and http_content_type.endswith('+xml')):
  2223.         acceptable_content_type = 1
  2224.         true_encoding = http_encoding or xml_encoding or 'utf-8'
  2225.     elif (http_content_type in text_content_types) or \
  2226.          (http_content_type.startswith('text/')) and http_content_type.endswith('+xml'):
  2227.         acceptable_content_type = 1
  2228.         true_encoding = http_encoding or 'us-ascii'
  2229.     elif http_content_type.startswith('text/'):
  2230.         true_encoding = http_encoding or 'us-ascii'
  2231.     elif http_headers and (not http_headers.has_key('content-type')):
  2232.         true_encoding = xml_encoding or 'iso-8859-1'
  2233.     else:
  2234.         true_encoding = xml_encoding or 'utf-8'
  2235.     return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
  2236.     
  2237. def _toUTF8(data, encoding):
  2238.     """Changes an XML data stream on the fly to specify a new encoding
  2239.  
  2240.     data is a raw sequence of bytes (not Unicode) that is presumed to be in %encoding already
  2241.     encoding is a string recognized by encodings.aliases
  2242.     """
  2243.     if _debug: sys.stderr.write('entering _toUTF8, trying encoding %s\n' % encoding)
  2244.     # strip Byte Order Mark (if present)
  2245.     if (len(data) >= 4) and (data[:2] == '\xfe\xff') and (data[2:4] != '\x00\x00'):
  2246.         if _debug:
  2247.             sys.stderr.write('stripping BOM\n')
  2248.             if encoding != 'utf-16be':
  2249.                 sys.stderr.write('trying utf-16be instead\n')
  2250.         encoding = 'utf-16be'
  2251.         data = data[2:]
  2252.     elif (len(data) >= 4) and (data[:2] == '\xff\xfe') and (data[2:4] != '\x00\x00'):
  2253.         if _debug:
  2254.             sys.stderr.write('stripping BOM\n')
  2255.             if encoding != 'utf-16le':
  2256.                 sys.stderr.write('trying utf-16le instead\n')
  2257.         encoding = 'utf-16le'
  2258.         data = data[2:]
  2259.     elif data[:3] == '\xef\xbb\xbf':
  2260.         if _debug:
  2261.             sys.stderr.write('stripping BOM\n')
  2262.             if encoding != 'utf-8':
  2263.                 sys.stderr.write('trying utf-8 instead\n')
  2264.         encoding = 'utf-8'
  2265.         data = data[3:]
  2266.     elif data[:4] == '\x00\x00\xfe\xff':
  2267.         if _debug:
  2268.             sys.stderr.write('stripping BOM\n')
  2269.             if encoding != 'utf-32be':
  2270.                 sys.stderr.write('trying utf-32be instead\n')
  2271.         encoding = 'utf-32be'
  2272.         data = data[4:]
  2273.     elif data[:4] == '\xff\xfe\x00\x00':
  2274.         if _debug:
  2275.             sys.stderr.write('stripping BOM\n')
  2276.             if encoding != 'utf-32le':
  2277.                 sys.stderr.write('trying utf-32le instead\n')
  2278.         encoding = 'utf-32le'
  2279.         data = data[4:]
  2280.     newdata = unicode(data, encoding)
  2281.     if _debug: sys.stderr.write('successfully converted %s data to unicode\n' % encoding)
  2282.     declmatch = re.compile('^<\?xml[^>]*?>')
  2283.     newdecl = """<?xml version='1.0' encoding='utf-8'?>"""
  2284.     if declmatch.search(newdata):
  2285.         newdata = declmatch.sub(newdecl, newdata)
  2286.     else:
  2287.         newdata = newdecl + u'\n' + newdata
  2288.     return newdata.encode("utf-8")
  2289.  
  2290. def _stripDoctype(data):
  2291.     """Strips DOCTYPE from XML document, returns (rss_version, stripped_data)
  2292.  
  2293.     rss_version may be "rss091n" or None
  2294.     stripped_data is the same XML document, minus the DOCTYPE
  2295.     """
  2296.     entity_pattern = re.compile(r'<!ENTITY([^>]*?)>', re.MULTILINE)
  2297.     data = entity_pattern.sub('', data)
  2298.     doctype_pattern = re.compile(r'<!DOCTYPE([^>]*?)>', re.MULTILINE)
  2299.     doctype_results = doctype_pattern.findall(data)
  2300.     doctype = doctype_results and doctype_results[0] or ''
  2301.     if doctype.lower().count('netscape'):
  2302.         version = 'rss091n'
  2303.     else:
  2304.         version = None
  2305.     data = doctype_pattern.sub('', data)
  2306.     return version, data
  2307.     
  2308. def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[]):
  2309.     """Parse a feed from a URL, file, stream, or string"""
  2310.     result = FeedParserDict()
  2311.     result['feed'] = FeedParserDict()
  2312.     result['entries'] = []
  2313.     if _XML_AVAILABLE:
  2314.         result['bozo'] = 0
  2315.     if type(handlers) == types.InstanceType:
  2316.         handlers = [handlers]
  2317.     try:
  2318.         f = _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, handlers)
  2319.         data = f.read()
  2320.     except Exception, e:
  2321.         result['bozo'] = 1
  2322.         result['bozo_exception'] = e
  2323.         data = ''
  2324.         f = None
  2325.  
  2326.     # if feed is gzip-compressed, decompress it
  2327.     if f and data and hasattr(f, "headers"):
  2328.         if gzip and f.headers.get('content-encoding', '') == 'gzip':
  2329.             try:
  2330.                 data = gzip.GzipFile(fileobj=_StringIO(data)).read()
  2331.             except Exception, e:
  2332.                 # Some feeds claim to be gzipped but they're not, so
  2333.                 # we get garbage.  Ideally, we should re-request the
  2334.                 # feed without the "Accept-encoding: gzip" header,
  2335.                 # but we don't.
  2336.                 result['bozo'] = 1
  2337.                 result['bozo_exception'] = e
  2338.                 data = ''
  2339.         elif zlib and f.headers.get('content-encoding', '') == 'deflate':
  2340.             try:
  2341.                 data = zlib.decompress(data, -zlib.MAX_WBITS)
  2342.             except Exception, e:
  2343.                 result['bozo'] = 1
  2344.                 result['bozo_exception'] = e
  2345.                 data = ''
  2346.  
  2347.     # save HTTP headers
  2348.     if hasattr(f, "info"):
  2349.         info = f.info()
  2350.         result["etag"] = info.getheader("ETag")
  2351.         last_modified = info.getheader("Last-Modified")
  2352.         if last_modified:
  2353.             result["modified"] = _parse_date(last_modified)
  2354.     if hasattr(f, "url"):
  2355.         result["url"] = f.url
  2356.         result["status"] = 200
  2357.     if hasattr(f, "status"):
  2358.         result["status"] = f.status
  2359.     if hasattr(f, "headers"):
  2360.         result["headers"] = f.headers.dict
  2361.     if hasattr(f, "close"):
  2362.         f.close()
  2363.  
  2364.     # there are four encodings to keep track of:
  2365.     # - http_encoding is the encoding declared in the Content-Type HTTP header
  2366.     # - xml_encoding is the encoding declared in the <?xml declaration
  2367.     # - sniffed_encoding is the encoding sniffed from the first 4 bytes of the XML data
  2368.     # - result['encoding'] is the actual encoding, as per RFC 3023 and a variety of other conflicting specifications
  2369.     http_headers = result.get("headers", {})
  2370.     result['encoding'], http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type = \
  2371.         _getCharacterEncoding(http_headers, data)
  2372.     if http_headers and (not acceptable_content_type):
  2373.         if http_headers.has_key('content-type'):
  2374.             bozo_message = '%s is not an XML media type' % http_headers['content-type']
  2375.         else:
  2376.             bozo_message = 'no Content-type specified'
  2377.         result['bozo'] = 1
  2378.         result['bozo_exception'] = NonXMLContentType(bozo_message)
  2379.         
  2380.     result['version'], data = _stripDoctype(data)
  2381.  
  2382.     baseuri = http_headers.get('content-location', result.get('url'))
  2383.     baselang = http_headers.get('content-language', None)
  2384.  
  2385.     # if server sent 304, we're done
  2386.     if result.get("status", 0) == 304:
  2387.         result['version'] = ''
  2388.         result['debug_message'] = "The feed has not changed since you last checked, " + \
  2389.             "so the server sent no data.  This is a feature, not a bug!"
  2390.         return result
  2391.  
  2392.     # if there was a problem downloading, we're done
  2393.     if not data:
  2394.         return result
  2395.  
  2396.     # determine character encoding
  2397.     use_strict_parser = 0
  2398.     known_encoding = 0
  2399.     tried_encodings = []
  2400.     for proposed_encoding in (result['encoding'], xml_encoding, sniffed_xml_encoding, 'utf-8', 'windows-1252'):
  2401.         if proposed_encoding in tried_encodings: continue
  2402.         if not proposed_encoding: continue
  2403.         try:
  2404.             data = _toUTF8(data, proposed_encoding)
  2405.             known_encoding = 1
  2406.             use_strict_parser = 1
  2407.             break
  2408.         except:
  2409.             pass
  2410.         tried_encodings.append(proposed_encoding)
  2411.     if not known_encoding:
  2412.         result['bozo'] = 1
  2413.         result['bozo_exception'] = CharacterEncodingUnknown( \
  2414.             "document encoding unknown, I tried " + \
  2415.             "%s, %s, utf-8, and windows-1252 but nothing worked" % \
  2416.             (result['encoding'], xml_encoding))
  2417.         result['encoding'] = ''
  2418.     elif proposed_encoding != result['encoding']:
  2419.         result['bozo'] = 1
  2420.         result['bozo_exception'] = CharacterEncodingOverride( \
  2421.             "documented declared as %s, but parsed as %s" % \
  2422.             (result['encoding'], proposed_encoding))
  2423.         result['encoding'] = proposed_encoding
  2424.  
  2425.     if not _XML_AVAILABLE:
  2426.         use_strict_parser = 0
  2427.     if use_strict_parser:
  2428.         # initialize the SAX parser
  2429.         feedparser = _StrictFeedParser(baseuri, baselang, 'utf-8')
  2430.         saxparser = xml.sax.make_parser(PREFERRED_XML_PARSERS)
  2431.         saxparser.setFeature(xml.sax.handler.feature_namespaces, 1)
  2432.         saxparser.setContentHandler(feedparser)
  2433.         saxparser.setErrorHandler(feedparser)
  2434.         source = xml.sax.xmlreader.InputSource()
  2435.         source.setByteStream(_StringIO(data))
  2436.         if hasattr(saxparser, '_ns_stack'):
  2437.             # work around bug in built-in SAX parser (doesn't recognize xml: namespace)
  2438.             # PyXML doesn't have this problem, and it doesn't have _ns_stack either
  2439.             saxparser._ns_stack.append({'http://www.w3.org/XML/1998/namespace':'xml'})
  2440.         try:
  2441.             saxparser.parse(source)
  2442.         except Exception, e:
  2443.             if _debug:
  2444.                 import traceback
  2445.                 traceback.print_stack()
  2446.                 traceback.print_exc()
  2447.                 sys.stderr.write('xml parsing failed\n')
  2448.             result['bozo'] = 1
  2449.             result['bozo_exception'] = feedparser.exc or e
  2450.             use_strict_parser = 0
  2451.     if not use_strict_parser:
  2452.         feedparser = _LooseFeedParser(baseuri, baselang, known_encoding and 'utf-8' or '')
  2453.         feedparser.feed(data)
  2454.     result['feed'] = feedparser.feeddata
  2455.     result['entries'] = feedparser.entries
  2456.     result['version'] = result['version'] or feedparser.version
  2457.     return result
  2458.  
  2459. if __name__ == '__main__':
  2460.     if not sys.argv[1:]:
  2461.         print __doc__
  2462.         sys.exit(0)
  2463.     else:
  2464.         urls = sys.argv[1:]
  2465.     zopeCompatibilityHack()
  2466.     from pprint import pprint
  2467.     for url in urls:
  2468.         print url
  2469.         print
  2470.         result = parse(url)
  2471.         pprint(result)
  2472.         print
  2473.  
  2474. #REVISION HISTORY
  2475. #1.0 - 9/27/2002 - MAP - fixed namespace processing on prefixed RSS 2.0 elements,
  2476. #  added Simon Fell's test suite
  2477. #1.1 - 9/29/2002 - MAP - fixed infinite loop on incomplete CDATA sections
  2478. #2.0 - 10/19/2002
  2479. #  JD - use inchannel to watch out for image and textinput elements which can
  2480. #  also contain title, link, and description elements
  2481. #  JD - check for isPermaLink="false" attribute on guid elements
  2482. #  JD - replaced openAnything with open_resource supporting ETag and
  2483. #  If-Modified-Since request headers
  2484. #  JD - parse now accepts etag, modified, agent, and referrer optional
  2485. #  arguments
  2486. #  JD - modified parse to return a dictionary instead of a tuple so that any
  2487. #  etag or modified information can be returned and cached by the caller
  2488. #2.0.1 - 10/21/2002 - MAP - changed parse() so that if we don't get anything
  2489. #  because of etag/modified, return the old etag/modified to the caller to
  2490. #  indicate why nothing is being returned
  2491. #2.0.2 - 10/21/2002 - JB - added the inchannel to the if statement, otherwise its
  2492. #  useless.  Fixes the problem JD was addressing by adding it.
  2493. #2.1 - 11/14/2002 - MAP - added gzip support
  2494. #2.2 - 1/27/2003 - MAP - added attribute support, admin:generatorAgent.
  2495. #  start_admingeneratoragent is an example of how to handle elements with
  2496. #  only attributes, no content.
  2497. #2.3 - 6/11/2003 - MAP - added USER_AGENT for default (if caller doesn't specify);
  2498. #  also, make sure we send the User-Agent even if urllib2 isn't available.
  2499. #  Match any variation of backend.userland.com/rss namespace.
  2500. #2.3.1 - 6/12/2003 - MAP - if item has both link and guid, return both as-is.
  2501. #2.4 - 7/9/2003 - MAP - added preliminary Pie/Atom/Echo support based on Sam Ruby's
  2502. #  snapshot of July 1 <http://www.intertwingly.net/blog/1506.html>; changed
  2503. #  project name
  2504. #2.5 - 7/25/2003 - MAP - changed to Python license (all contributors agree);
  2505. #  removed unnecessary urllib code -- urllib2 should always be available anyway;
  2506. #  return actual url, status, and full HTTP headers (as result['url'],
  2507. #  result['status'], and result['headers']) if parsing a remote feed over HTTP --
  2508. #  this should pass all the HTTP tests at <http://diveintomark.org/tests/client/http/>;
  2509. #  added the latest namespace-of-the-week for RSS 2.0
  2510. #2.5.1 - 7/26/2003 - RMK - clear opener.addheaders so we only send our custom
  2511. #  User-Agent (otherwise urllib2 sends two, which confuses some servers)
  2512. #2.5.2 - 7/28/2003 - MAP - entity-decode inline xml properly; added support for
  2513. #  inline <xhtml:body> and <xhtml:div> as used in some RSS 2.0 feeds
  2514. #2.5.3 - 8/6/2003 - TvdV - patch to track whether we're inside an image or
  2515. #  textInput, and also to return the character encoding (if specified)
  2516. #2.6 - 1/1/2004 - MAP - dc:author support (MarekK); fixed bug tracking
  2517. #  nested divs within content (JohnD); fixed missing sys import (JohanS);
  2518. #  fixed regular expression to capture XML character encoding (Andrei);
  2519. #  added support for Atom 0.3-style links; fixed bug with textInput tracking;
  2520. #  added support for cloud (MartijnP); added support for multiple
  2521. #  category/dc:subject (MartijnP); normalize content model: "description" gets
  2522. #  description (which can come from description, summary, or full content if no
  2523. #  description), "content" gets dict of base/language/type/value (which can come
  2524. #  from content:encoded, xhtml:body, content, or fullitem);
  2525. #  fixed bug matching arbitrary Userland namespaces; added xml:base and xml:lang
  2526. #  tracking; fixed bug tracking unknown tags; fixed bug tracking content when
  2527. #  <content> element is not in default namespace (like Pocketsoap feed);
  2528. #  resolve relative URLs in link, guid, docs, url, comments, wfw:comment,
  2529. #  wfw:commentRSS; resolve relative URLs within embedded HTML markup in
  2530. #  description, xhtml:body, content, content:encoded, title, subtitle,
  2531. #  summary, info, tagline, and copyright; added support for pingback and
  2532. #  trackback namespaces
  2533. #2.7 - 1/5/2004 - MAP - really added support for trackback and pingback
  2534. #  namespaces, as opposed to 2.6 when I said I did but didn't really;
  2535. #  sanitize HTML markup within some elements; added mxTidy support (if
  2536. #  installed) to tidy HTML markup within some elements; fixed indentation
  2537. #  bug in _parse_date (FazalM); use socket.setdefaulttimeout if available
  2538. #  (FazalM); universal date parsing and normalization (FazalM): 'created', modified',
  2539. #  'issued' are parsed into 9-tuple date format and stored in 'created_parsed',
  2540. #  'modified_parsed', and 'issued_parsed'; 'date' is duplicated in 'modified'
  2541. #  and vice-versa; 'date_parsed' is duplicated in 'modified_parsed' and vice-versa
  2542. #2.7.1 - 1/9/2004 - MAP - fixed bug handling " and '.  fixed memory
  2543. #  leak not closing url opener (JohnD); added dc:publisher support (MarekK);
  2544. #  added admin:errorReportsTo support (MarekK); Python 2.1 dict support (MarekK)
  2545. #2.7.4 - 1/14/2004 - MAP - added workaround for improperly formed <br/> tags in
  2546. #  encoded HTML (skadz); fixed unicode handling in normalize_attrs (ChrisL);
  2547. #  fixed relative URI processing for guid (skadz); added ICBM support; added
  2548. #  base64 support
  2549. #2.7.5 - 1/15/2004 - MAP - added workaround for malformed DOCTYPE (seen on many
  2550. #  blogspot.com sites); added _debug variable
  2551. #2.7.6 - 1/16/2004 - MAP - fixed bug with StringIO importing
  2552. #3.0b3 - 1/23/2004 - MAP - parse entire feed with real XML parser (if available);
  2553. #  added several new supported namespaces; fixed bug tracking naked markup in
  2554. #  description; added support for enclosure; added support for source; re-added
  2555. #  support for cloud which got dropped somehow; added support for expirationDate
  2556. #3.0b4 - 1/26/2004 - MAP - fixed xml:lang inheritance; fixed multiple bugs tracking
  2557. #  xml:base URI, one for documents that don't define one explicitly and one for
  2558. #  documents that define an outer and an inner xml:base that goes out of scope
  2559. #  before the end of the document
  2560. #3.0b5 - 1/26/2004 - MAP - fixed bug parsing multiple links at feed level
  2561. #3.0b6 - 1/27/2004 - MAP - added feed type and version detection, result["version"]
  2562. #  will be one of SUPPORTED_VERSIONS.keys() or empty string if unrecognized;
  2563. #  added support for creativeCommons:license and cc:license; added support for
  2564. #  full Atom content model in title, tagline, info, copyright, summary; fixed bug
  2565. #  with gzip encoding (not always telling server we support it when we do)
  2566. #3.0b7 - 1/28/2004 - MAP - support Atom-style author element in author_detail
  2567. #  (dictionary of "name", "url", "email"); map author to author_detail if author
  2568. #  contains name + email address
  2569. #3.0b8 - 1/28/2004 - MAP - added support for contributor
  2570. #3.0b9 - 1/29/2004 - MAP - fixed check for presence of dict function; added
  2571. #  support for summary
  2572. #3.0b10 - 1/31/2004 - MAP - incorporated ISO-8601 date parsing routines from
  2573. #  xml.util.iso8601
  2574. #3.0b11 - 2/2/2004 - MAP - added 'rights' to list of elements that can contain
  2575. #  dangerous markup; fiddled with decodeEntities (not right); liberalized
  2576. #  date parsing even further
  2577. #3.0b12 - 2/6/2004 - MAP - fiddled with decodeEntities (still not right);
  2578. #  added support to Atom 0.2 subtitle; added support for Atom content model
  2579. #  in copyright; better sanitizing of dangerous HTML elements with end tags
  2580. #  (script, frameset)
  2581. #3.0b13 - 2/8/2004 - MAP - better handling of empty HTML tags (br, hr, img,
  2582. #  etc.) in embedded markup, in either HTML or XHTML form (<br>, <br/>, <br />)
  2583. #3.0b14 - 2/8/2004 - MAP - fixed CDATA handling in non-wellformed feeds under
  2584. #  Python 2.1
  2585. #3.0b15 - 2/11/2004 - MAP - fixed bug resolving relative links in wfw:commentRSS;
  2586. #  fixed bug capturing author and contributor URL; fixed bug resolving relative
  2587. #  links in author and contributor URL; fixed bug resolvin relative links in
  2588. #  generator URL; added support for recognizing RSS 1.0; passed Simon Fell's
  2589. #  namespace tests, and included them permanently in the test suite with his
  2590. #  permission; fixed namespace handling under Python 2.1
  2591. #3.0b16 - 2/12/2004 - MAP - fixed support for RSS 0.90 (broken in b15)
  2592. #3.0b17 - 2/13/2004 - MAP - determine character encoding as per RFC 3023
  2593. #3.0b18 - 2/17/2004 - MAP - always map description to summary_detail (Andrei);
  2594. #  use libxml2 (if available)
  2595. #3.0b19 - 3/15/2004 - MAP - fixed bug exploding author information when author
  2596. #  name was in parentheses; removed ultra-problematic mxTidy support; patch to
  2597. #  workaround crash in PyXML/expat when encountering invalid entities
  2598. #  (MarkMoraes); support for textinput/textInput
  2599. #3.0b20 - 4/7/2004 - MAP - added CDF support
  2600. #3.0b21 - 4/14/2004 - MAP - added Hot RSS support
  2601. #3.0b22 - 4/19/2004 - MAP - changed 'channel' to 'feed', 'item' to 'entries' in
  2602. #  results dict; changed results dict to allow getting values with results.key
  2603. #  as well as results[key]; work around embedded illformed HTML with half
  2604. #  a DOCTYPE; work around malformed Content-Type header; if character encoding
  2605. #  is wrong, try several common ones before falling back to regexes (if this
  2606. #  works, bozo_exception is set to CharacterEncodingOverride); fixed character
  2607. #  encoding issues in BaseHTMLProcessor by tracking encoding and converting
  2608. #  from Unicode to raw strings before feeding data to sgmllib.SGMLParser;
  2609. #  convert each value in results to Unicode (if possible), even if using
  2610. #  regex-based parsing
  2611. #3.0b23 - 4/21/2004 - MAP - fixed UnicodeDecodeError for feeds that contain
  2612. #  high-bit characters in attributes in embedded HTML in description (thanks
  2613. #  Thijs van de Vossen); moved guid, date, and date_parsed to mapped keys in
  2614. #  FeedParserDict; tweaked FeedParserDict.has_key to return True if asking
  2615. #  about a mapped key
  2616. #3.0fc1 - 4/23/2004 - MAP - made results.entries[0].links[0] and
  2617. #  results.entries[0].enclosures[0] into FeedParserDict; fixed typo that could
  2618. #  cause the same encoding to be tried twice (even if it failed the first time);
  2619. #  fixed DOCTYPE stripping when DOCTYPE contained entity declarations;
  2620. #  better textinput and image tracking in illformed RSS 1.0 feeds
  2621. #3.0fc2 - 5/10/2004 - MAP - added and passed Sam's amp tests; added and passed
  2622. #  my blink tag tests
  2623. #3.0fc3 - 6/18/2004 - MAP - fixed bug in _changeEncodingDeclaration that
  2624. #  failed to parse utf-16 encoded feeds; made source into a FeedParserDict;
  2625. #  duplicate admin:generatorAgent/@rdf:resource in generator_detail.url;
  2626. #  added support for image; refactored parse() fallback logic to try other
  2627. #  encodings if SAX parsing fails (previously it would only try other encodings
  2628. #  if re-encoding failed); remove unichr madness in normalize_attrs now that
  2629. #  we're properly tracking encoding in and out of BaseHTMLProcessor; set
  2630. #  feed.language from root-level xml:lang; set entry.id from rdf:about;
  2631. #  send Accept header
  2632. #3.0 - 6/21/2004 - MAP - don't try iso-8859-1 (can't distinguish between
  2633. #  iso-8859-1 and windows-1252 anyway, and most incorrectly marked feeds are
  2634. #  windows-1252); fixed regression that could cause the same encoding to be
  2635. #  tried twice (even if it failed the first time)
  2636. #3.0.1 - 6/22/2004 - MAP - default to us-ascii for all text/* content types;
  2637. #  recover from malformed content-type header parameter with no equals sign
  2638. #  ("text/xml; charset:iso-8859-1")
  2639. #3.1 - 6/28/2004 - MAP - added and passed tests for converting HTML entities
  2640. #  to Unicode equivalents in illformed feeds (aaronsw); added and
  2641. #  passed tests for converting character entities to Unicode equivalents
  2642. #  in illformed feeds (aaronsw); test for valid parsers when setting
  2643. #  XML_AVAILABLE; make version and encoding available when server returns
  2644. #  a 304; add handlers parameter to pass arbitrary urllib2 handlers (like
  2645. #  digest auth or proxy support); add code to parse username/password
  2646. #  out of url and send as basic authentication; expose downloading-related
  2647. #  exceptions in bozo_exception (aaronsw); added __contains__ method to
  2648. #  FeedParserDict (aaronsw); added publisher_detail (aaronsw)
  2649. #3.2 - 7/3/2004 - MAP - use cjkcodecs and iconv_codec if available; always
  2650. #  convert feed to UTF-8 before passing to XML parser; completely revamped
  2651. #  logic for determining character encoding and attempting XML parsing
  2652. #  (much faster); increased default timeout to 20 seconds; test for presence
  2653. #  of Location header on redirects; added tests for many alternate character
  2654. #  encodings; support various EBCDIC encodings; support UTF-16BE and
  2655. #  UTF16-LE with or without a BOM; support UTF-8 with a BOM; support
  2656. #  UTF-32BE and UTF-32LE with or without a BOM; fixed crashing bug if no
  2657. #  XML parsers are available; added support for "Content-encoding: deflate";
  2658. #  send blank "Accept-encoding: " header if neither gzip nor zlib modules
  2659. #  are available
  2660. #3.3 - 7/15/2004 - MAP - optimize EBCDIC to ASCII conversion; fix obscure
  2661. #  problem tracking xml:base and xml:lang if element declares it, child
  2662. #  doesn't, first grandchild redeclares it, and second grandchild doesn't;
  2663. #  refactored date parsing; defined public registerDateHandler so callers
  2664. #  can add support for additional date formats at runtime; added support
  2665. #  for OnBlog, Nate, MSSQL, Greek, and Hungarian dates (ytrewq1); added
  2666. #  zopeCompatibilityHack() which turns FeedParserDict into a regular
  2667. #  dictionary, required for Zope compatibility, and also makes command-
  2668. #  line debugging easier because pprint module formats real dictionaries
  2669. #  better than dictionary-like objects; added NonXMLContentType exception,
  2670. #  which is stored in bozo_exception when a feed is served with a non-XML
  2671. #  media type such as "text/plain"; respect Content-Language as default
  2672. #  language if not xml:lang is present; cloud dict is now FeedParserDict;
  2673. #  generator dict is now FeedParserDict; better tracking of xml:lang,
  2674. #  including support for xml:lang="" to unset the current language;
  2675. #  recognize RSS 1.0 feeds even when RSS 1.0 namespace is not the default
  2676. #  namespace; don't overwrite final status on redirects (scenarios:
  2677. #  redirecting to a URL that returns 304, redirecting to a URL that
  2678. #  redirects to another URL with a different type of redirect); add
  2679. #  support for HTTP 303 redirects
  2680.